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;
}
}
String jsonString = "{\"id\":2,\"name\":\"guest\"}\n";
// 反序列化代码示例1
Object ob1 = JSON.parse(jsonString);
System.out.println("ob1="+ob1);
// 反序列化代码示例2
Object ob2 = JSON.parseObject(jsonString);
System.out.println("ob2="+ob2);
// 反序列化代码示例3
Object 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.java
public 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 set
if (!(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 methods
String methodName = method.getName();
// 长度不能小于4
if (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不为null
if (_name == null) return null;
// 第二个条件 _class不为null,进到defineTransletClasses里
if (_class == null) defineTransletClasses();
// ......
}
}
第一个条件就是_name不为null,第二个条件是_class不为null,这样才能进到defineTransletClasses方法,我们继续来看看defineTransletClasses方法里边的判断:
private void defineTransletClasses()
throws TransformerConfigurationException {
// 第三个条件:_bytecodes不为null
if (_bytecodes == null) {
ErrorMsg err = new ErrorMsg(ErrorMsg.NO_TRANSLET_CLASS_ERR);
throw new TransformerConfigurationException(err.toString());
}
// 7u21
TransletClassLoader loader = (TransletClassLoader)
AccessController.doPrivileged(new PrivilegedAction() {
public Object run() {
// 这里应该是第四个条件,这里7u21和高版本的7u80有区别
return new TransletClassLoader(ObjectFactory.findClassLoader());
}
});
// 7u80
TransletClassLoader 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链的分析。
征稿通知
知识应该被分享,安全更需携手共进
RECRUITMENT
招聘启事
END



长按识别二维码关注我们