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方法条件
  1. 方法名长度大于4且以set开头,且第四个字母要是大写
  2. 非静态方法
  3. 返回类型为void或当前类
  4. 参数个数为1个
  • get方法条件
  1. 方法名长度大于等于4,且以get开头且第4个字母为大写
  2. 非静态方法
  3. 无传入参数
  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对应的类的静态方法或者构造方法里,这样,我们就集齐了全部的限制条件,小结一下就是:

  1. name != null
  2. _class == null
  3. _bytecodes != null
  4. _tfactory需要有一个getExternalExtensionsMap方法
  5. _bytecodes的类必须是ABSTRACT_TRANSLET的子类
  6. 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链的分析。