fastjson1224的TemplatesImpl鏈反序列化分析
STATEMENT
聲明
由于傳播、利用此文所提供的信息而造成的任何直接或者間接的后果及損失,均由使用者本人負責,雷神眾測及文章作者不為此承擔任何責任。
雷神眾測擁有對此文章的修改和解釋權。如欲轉載或傳播此文章,必須保證此文章的完整性,包括版權聲明等全部內容。未經雷神眾測允許,不得任意修改或者增減此文章內容,不得以任何方式將其用于商業目的。
fastjson的序列化與反序列化
首先,分析fastjson的反序列化前我們先來了解一下fastjson,我們新建一個User類:
public class User {public Long id;public String name;public Long getId() {System.out.println("getid="+this.id);return id;}public void setId(Long id) {this.id = id;System.out.println("setid="+this.id);}public String getName() {System.out.println("getname="+this.name);return name;}public void setName(String name) {System.out.println("setname="+this.name);this.name = name;}}
fastjson提供了三種反序列化的方法,分別是:
String jsonString = "{\"id\":2,\"name\":\"guest\"}\n";// 反序列化代碼示例1Object ob1 = JSON.parse(jsonString);System.out.println("ob1="+ob1);// 反序列化代碼示例2Object ob2 = JSON.parseObject(jsonString);System.out.println("ob2="+ob2);// 反序列化代碼示例3Object ob3 = JSON.parseObject(jsonString,User.class);System.out.println("ob3="+ob3);
那么這三個方法有什么區別呢,接下來,我們在代碼中set一下參數的值,運行一遍看看:
public class main {public static void main(String[] args) {User guestUser = new User();guestUser.setId(2L);guestUser.setName("guest");// fastjson提供toJsonString接口實現序列化// fastjson提供parseObject來分別實現反序列化// 序列化示例String jsonString = JSON.toJSONString(guestUser);System.out.println("序列化數據="+jsonString);...}}

運行后發現ob1和ob2返回的是JSONObject,而ob3則是實際的類對象,并且ob3調用了User類中全部set方法,ob1與ob2則是set、get方法均未調用,那么接下來我們就來看看他們在調用set與get方法的區別
我們新寫一段代碼,首先傳入User類的一句序列化后的字符串,而后使用三種方法來反序列化它,看看分別調用了什么方法
String parseString = "{\"@type\":\"User\",\"id\":2,\"name\":\"guest\"}";System.out.println("parse方法的調用結果");Object ob11 = JSON.parse(parseString);System.out.println("ob11="+ob11);System.out.println("----------------------");System.out.println("parseObject方法的調用結果");Object ob12 = JSON.parseObject(parseString);System.out.println("ob12="+ob12);System.out.println("----------------------");System.out.println("parseObject(xx,class)方法的調用結果");Object ob13 = JSON.parseObject(parseString,User.class);System.out.println("ob13="+ob13);
結果:

我們可以發現,parse和parseObject都返回了全部的set方法,并且parseObject返回了全部的get方法,parseObject(xx,class)返回了全部的set方法,那么我們是否可以得出結論,全部的方法都可以調用set方法呢?那么接下來我們就來看看調用到的set、get方法分別存在什么限制,這其實就是fastjson的特性
fastjson特性
首先,我們來看set與get方法的限制:
// com/alibaba/fastjson/util/JavaBeanInfo.javapublic class JavaBeanInfo{public static JavaBeanInfo build(Class clazz, Type type, PropertyNamingStrategy propertyNamingStrategy) {/*** 判斷set方法函數*/for (Method method : methods) { //int ordinal = 0, serialzeFeatures = 0, parserFeatures = 0;String methodName = method.getName();// 長度比4大if (methodName.length() < 4) {continue;}// 非靜態方法if (Modifier.isStatic(method.getModifiers())) {continue;}// 返回類型不能是void或者當前類// support builder setif (!(method.getReturnType().equals(Void.TYPE) || method.getReturnType().equals(method.getDeclaringClass()))) {continue;}Class[] types = method.getParameterTypes();// 不能有傳入的參數if (types.length != 1) {continue;}// annotation = null,跳過JSONField annotation = method.getAnnotation(JSONField.class);if (annotation == null) {annotation = TypeUtils.getSuperMethodAnnotation(clazz, method);}if (annotation != null) {// ......略過}// 以set為開頭if (!methodName.startsWith("set")) {continue;}char c3 = methodName.charAt(3);String propertyName;// 第4個字母大寫if (Character.isUpperCase(c3) //|| c3 > 512 // for unicode method name) {if (TypeUtils.compatibleWithJavaBean) {propertyName = TypeUtils.decapitalize(methodName.substring(3));} else {propertyName = Character.toLowerCase(methodName.charAt(3)) + methodName.substring(4);}} else if (c3 == '_') {propertyName = methodName.substring(4);} else if (c3 == 'f') {propertyName = methodName.substring(3);} else if (methodName.length() >= 5 && Character.isUpperCase(methodName.charAt(4))) {propertyName = TypeUtils.decapitalize(methodName.substring(3));} else {continue;}Field field = TypeUtils.getField(clazz, propertyName, declaredFields);if (field == null && types[0] == boolean.class) {String isFieldName = "is" + Character.toUpperCase(propertyName.charAt(0)) + propertyName.substring(1);field = TypeUtils.getField(clazz, isFieldName, declaredFields);}// filed = null,跳過JSONField fieldAnnotation = null;if (field != null) {// ......}// propertyNamingStrategy = null,跳過if (propertyNamingStrategy != null) {// ......}add(fieldList, new FieldInfo(propertyName, method, field, clazz, type, ordinal, serialzeFeatures, parserFeatures,annotation, fieldAnnotation, null));}/*** 判斷get方法函數體:*/for (Method method : clazz.getMethods()) { // getter methodsString methodName = method.getName();// 長度不能小于4if (methodName.length() < 4) {continue;}// 不能是靜態方法if (Modifier.isStatic(method.getModifiers())) {continue;}// 以get開頭并且第4位是個大寫字母if (methodName.startsWith("get") && Character.isUpperCase(methodName.charAt(3))) {// 不能有傳入的參數if (method.getParameterTypes().length != 0) {continue;}// 返回值繼承于Collection Map AtomicBoolean AtomicInteger AtomicLong之一if (Collection.class.isAssignableFrom(method.getReturnType()) //|| Map.class.isAssignableFrom(method.getReturnType()) //|| AtomicBoolean.class == method.getReturnType() //|| AtomicInteger.class == method.getReturnType() //|| AtomicLong.class == method.getReturnType() //) {// .........// 對get方法進行處理的函數,略過}
所以,根據以上代碼,我們可以總結出調用到set、get方法的限制:
- set方法條件
- 方法名長度大于4且以set開頭,且第四個字母要是大寫
- 非靜態方法
- 返回類型為void或當前類
- 參數個數為1個
- get方法條件
- 方法名長度大于等于4,且以get開頭且第4個字母為大寫
- 非靜態方法
- 無傳入參數
- 返回值類型繼承自Collection Map AtomicBoolean AtomicInteger AtomicLong
并且,parseobject()這個方法中多了一步toJSON操作,會調用全部的get方法:

這樣,上面調用到的set、get方法就全部說通了。
下面我們看看fastjson的另一種特性,就是@type這個參數,開發者本意是序列化時使用SerializerFeature.WriteClassName會寫入類型信息,從而使反序列化時不會丟失類型信息,并且反序列化時自動進行類型識別,但是這就造成了一個問題,type參數可以指定反序列化任意的類,然后去調用set、get方法,我們來看看實際的代碼是如何運作的
在序列化是寫入類型信息,此時的序列化后的字符串就有@type參數:

我們在看看輸入@type參數,他的反序列化結果:

看似這種情況很正常,反序列化使用到set、get方法恢復數據,但是在可以調用任意類的情況下,這就很恐怖了,如果set或者get方法中有可利用的點的情況下,就會造成RCE。
jdk7u21TemplatesImpl鏈
說到fastjson的TemplatesImpl鏈,就不得不提jdk7u21的TemplatesImpl鏈,fastjson的這種利用方法其實和jdk7u21的TemplatesImpl鏈很像,jdk7u21由以下代碼出發RCE
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
public class jdk7u21_nekoc {public static void main(String[] args) throws Exception {TemplatesImpl calc = (TemplatesImpl) Gadgets.createTemplatesImpl("/System/Applications/Calculator.app/Contents/MacOS/Calculator");calc.getOutputProperties();}}
gadegets類是在ysoserial里拔出來的,跟進觸發漏洞的calc.getOutputProperties();方法,發現最終由obj.newinstance觸發惡意代碼,由于newinstance示例化會默認觸發static及構造方法,所以payload可以寫在這兩個中的其一,并且,這里注意,getOutputProperties方法正好符合fastjson中對get方法名字的限制。
我們這里不再具體分析jdk7u21,我們來跟進看一下該鏈子的一些限制條件,跟進代碼:
return newTransformer().getOutputProperties();
首先,我們發現:
com/sun/org/apache/xalan/internal/xsltc/trax/TemplatesImpl.java中,存在兩個限制條件:
public class TemplatesImpl {private Translet getTransletInstance()throws TransformerConfigurationException {try {// 第一個條件 _name不為nullif (_name == null) return null;
// 第二個條件 _class不為null,進到defineTransletClasses里if (_class == null) defineTransletClasses();// ......}}
第一個條件就是_name不為null,第二個條件是_class不為null,這樣才能進到defineTransletClasses方法,我們繼續來看看defineTransletClasses方法里邊的判斷:
private void defineTransletClasses()throws TransformerConfigurationException {
// 第三個條件:_bytecodes不為nullif (_bytecodes == null) {ErrorMsg err = new ErrorMsg(ErrorMsg.NO_TRANSLET_CLASS_ERR);throw new TransformerConfigurationException(err.toString());}
// 7u21TransletClassLoader loader = (TransletClassLoader)AccessController.doPrivileged(new PrivilegedAction() {public Object run() {// 這里應該是第四個條件,這里7u21和高版本的7u80有區別return new TransletClassLoader(ObjectFactory.findClassLoader());}});// 7u80TransletClassLoader loader = (TransletClassLoader)AccessController.doPrivileged(new PrivilegedAction() {public Object run() {// 這里就是第四個限制條件,_tfactory參數存在一個getExternalExtensionsMap方法return new TransletClassLoader(ObjectFactory.findClassLoader(),_tfactory.getExternalExtensionsMap());}});
try {final int classCount = _bytecodes.length;_class = new Class[classCount];
if (classCount > 1) {_auxClasses = new Hashtable();}
for (int i = 0; i < classCount; i++) {_class[i] = loader.defineClass(_bytecodes[i]);final Class superClass = _class[i].getSuperclass();
// Check if this is the main class// 第五個限制條件:_bytecodes必須是ABSTRACT_TRANSLET子類if (superClass.getName().equals(ABSTRACT_TRANSLET)) {_transletIndex = i;}else {_auxClasses.put(_class[i].getName(), _class[i]);}}
if (_transletIndex < 0) {ErrorMsg err= new ErrorMsg(ErrorMsg.NO_MAIN_TRANSLET_ERR, _name);throw new TransformerConfigurationException(err.toString());}}catch(Exception e){//...}}}
我們跟進代碼首先會發現第三個限制條件,即_bytecodes不為null,繼續向下看,這里的代碼版本不同,內容就不一樣,在7u21中,不存在有關_tfactory的限制,在7u80中,就存在該限制,這里要求_tfactory參數存在一個getExternalExtensionsMap方法,為了版本通用,所以在poc里加上對tfactory的限制條件,繼續向下看,我們可以看到第五個條件,即_bytecodes必須是ABSTRACT_TRANSLET子類,并且,payload要寫在_bytecode對應的類的靜態方法或者構造方法里,這樣,我們就集齊了全部的限制條件,小結一下就是:
- name != null
- _class == null
- _bytecodes != null
- _tfactory需要有一個getExternalExtensionsMap方法
- _bytecodes的類必須是ABSTRACT_TRANSLET的子類
- payload要在_bytecode對應的類的靜態方法或者構造方法里
1224 TemplatesImpl鏈
jdk7u21在fastjson1224的應用還需要一點點條件,用parseObject()時,必須用JSON.parseObject(jsonString, Feature.SupportNonPublicField);這樣的格式,必須要有Feature.SupportNonPublicField參數才可以,payload中有部分參數是private屬性,需要在這里設置一下才能被接受;使用parse()時,需要JSON.parse(jsonString,Feature.SupportNonPublicField); 這樣的格式。
在編寫payload時,需要用到javasist類,這里不再描述該類。
最終的部分payload:

看到最后的代碼可能會有些疑問,這里的_tfactory為什么是空而不是一個有getExternalExtensionsMap方法的類對象?_OutputProperties是怎么調用到getOutputProperties方法的?
我們來看看這兩個問題的解答。
首先是第一個,_tfactory為什么是空而不是一個有getExternalExtensionsMap方法的類對象呢,我們跟一下代碼,不難發現,解析這幾個參數的時候,如果發現參數值為空對象,就會新建一個該參數應有的格式的對象實例,將其賦值給該參數


那么,_tfactory應有的格式為啥是它呢?咱們看一下定義就知道了

所以,_tfactory其實本身就是TransformerFactoryImpl類的對象,賦值為空就系那個回家了一樣,完全是可以的。
接下來我們看看第二個問題,_OutputProperties是怎么調用到getOutputProperties方法的呢,我們跟進代碼就可以看到,在處理參數的時候,代碼會將_OutputProperties前面的下劃線替換為空,此時反序列化時就回去調用他的get方法,即getOutputProperties,所以其實poc生成中得_OutputProperties有沒有下劃線都是可以的,這樣我們就在fastjson1224版本中完成了TemplatesImpl鏈的分析。