两个特性
用户可控制反序列化的对象
user类如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57
| package org.example;
public class user { private String name; private int age; private String hobby;
public user() { System.out.println("user构造函数user(无参数)"); }
public user(String name, int age, String hobby) { System.out.println("user构造函数user(有参数)"); this.name = name; this.age = age; this.hobby = hobby; }
public String getName() { System.out.println("user调用了getName"); return name; }
public void setName(String name) { System.out.println("user调用了setName"); this.name = name; }
public int getAge() { System.out.println("user调用了getAge"); return age; }
public void setAge(int age) { System.out.println("user调用了setAge"); this.age = age; }
public String getHobby() { System.out.println("user调用了getHobby"); return hobby; }
public void setHobby(String hobby) { System.out.println("user调用了setHobby"); this.hobby = hobby; }
@Override public String toString() { return "user{" + "name='" + name + '\'' + ", age=" + age + ", hobby='" + hobby + '\'' + '}'; } }
|
然后是调用函数,我都是写在各种小test里,然后在main中调用
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| package org.example;
import com.alibaba.fastjson.JSON; import com.alibaba.fastjson.serializer.SerializerFeature;
public class Main { public static void main(String[] args) { test3(); } }
|
安全的使用方法:
先看正确的使用方法:
1 2 3 4 5 6
| public static void test3(){
String p1 = "{\"age\":18,\"hobby\":\"gaming\",\"name\":\"y\"}"; user tmpuser = JSON.parseObject(p1, user.class); System.out.println(tmpuser.getAge()); }
|
可以看到输出中,出现了
user构造函数user(无参数)
这是因为需要构造user对象tmpuser。
user调用了setAge user调用了setHobby user调用了setName
这三个出现,是因为在构造过程中,给age,name,hobby这三个属性赋值了。
user调用了getAg
18
是tmpuser.getAge()这一行输出的。
限制死了反序列化的类型为user,这种由开发者控制的反序列化的类型,是安全的。
不安全的使用方法:
因为fastjson有一个特性,可以由用户指定反序列化的类型,通过@type字段。可以先设置另一个user2对象
代码如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57
| package org.example;
public class user2 { private String name; private int age; private String hobby;
public user2() { System.out.println("user2构造函数user2(无参数)"); }
public user2(String name, int age, String hobby) { System.out.println("user2构造函数user2(有参数)"); this.name = name; this.age = age; this.hobby = hobby; }
public String getName() { System.out.println("user2调用了getName"); return name; }
public void setName(String name) { System.out.println("user2调用了setName"); this.name = name; }
public int getAge() { System.out.println("user2调用了getAge"); return age; }
public void setAge(int age) { System.out.println("user2调用了setAge"); this.age = age; }
public String getHobby() { System.out.println("user2调用了getHobby"); return hobby; }
public void setHobby(String hobby) { System.out.println("user2调用了setHobby"); this.hobby = hobby; }
@Override public String toString() { return "user{" + "name='" + name + '\'' + ", age=" + age + ", hobby='" + hobby + '\'' + '}'; } }
|
然后是调用代码:
1 2 3 4 5 6 7 8
| public static void test4() { String p1 = "{\"@type\":\"org.example.user\",\"age\":18,\"hobby\":\"gaming\",\"name\":\"y\"}"; String p2 = "{\"@type\":\"org.example.user2\",\"age\":18,\"hobby\":\"gaming\",\"name\":\"y\"}"; System.out.println("JSON.parse(p1):"); Object P1 = JSON.parse(p1); System.out.println("JSON.parse(p2):"); Object P2 = JSON.parse(p2); }
|
输出结果如下:
可以将test()理解为一个函数,用户输入序列号后的字符串交给服务器,服务器对字符串进行反序列化,这里输入的p1,p2就可以理解为调用了两次函数。
调用getName方法
但反序列化字符串的函数不是parse而是parseObject时,就会调用对象中的getter类方法,比如getName,getAge,getHobby。
1 2 3 4 5 6 7
| public static void test6() { String p1 = "{\"@type\":\"org.example.user\",\"age\":18,\"hobby\":\"gaming\",\"name\":\"y\"}"; System.out.println("JSON.parse(p1):"); Object P1 = JSON.parse(p1); System.out.println("JSON.parseObject(p1):"); Object P2 = JSON.parseObject(p1); }
|
在了解这两种机制后,就可能会出现下面这种情况:代码中出现了 JSON.parseObject(p1); 可能设计初衷只是想解析反序列化”{"age":18,"hobby":"gaming","name":"y"}”这样的字符串,但是因为这两个特性,就可能出现一些意想不到的结果
调用流程分析
先来捋顺正常的调用,再来看漏洞的调用
正常流程
识别@type
使用如下代码,
1 2 3 4 5
| public static void test7(){ String p1 = "{\"@type\":\"org.example.user\",\"age\":18,\"hobby\":\"gaming\",\"name\":\"y\"}"; System.out.println("JSON.parseObject(p1):"); Object P2 = JSON.parseObject(p1); }
|
断点下载 Object P2 = JSON.parseObject(p1);
前面都是普通的调来调去,一直到这里
1 2 3 4 5 6 7 8 9 10
| public Object parse(Object fieldName) { final JSONLexer lexer = this.lexer; switch (lexer.token()) { ………… ………… case LBRACE: JSONObject object = new JSONObject(lexer.isEnabled(Feature.OrderedField)); return parseObject(object, fieldName); ………… …………
|
这里的lexer是由之前JSON.java中的
DefaultJSONParser parser = new DefaultJSONParser(text, ParserConfig.getGlobalInstance(), features);
这一行定义和赋值
在swtich中,识别到第一个字符串是{
(也就是 LBRACE )后,通过
return parseObject(object, fieldName);
进入到了parseObject函数中,这个函数还是在当前DefaultJSONParser.java文件中
经过几个判断后,进入到try finally 代码块中,这里面是一个for(;;)死循环,然后可以看到有一个判断
1 2 3 4 5 6 7 8 9 10
| if (key == JSON.DEFAULT_TYPE_KEY && !lexer.isEnabled(Feature.DisableSpecialKeyDetect)) { String typeName = lexer.scanSymbol(symbolTable, '"'); Class<?> clazz = TypeUtils.loadClass(typeName, config.getDefaultClassLoader());
if (clazz == null) { object.put(JSON.DEFAULT_TYPE_KEY, typeName); continue; } ……………… ………………
|
JSON.DEFAULT_TYPE_KEY
是@type
lexer.isEnabled()会返回入参的mask字段,这边的Feature.DisableSpecialKeyDetect
内容如下,所以!lexer.isEnabled(Feature.DisableSpecialKeyDetect)的结果为true
随后就获取到@type的value:org.example.user
这边的意思大概就是,通过识别到{
开头,然后开始进行反序列化的字符从识别,识别到@type,意味着需要指定类进行java反序列化。这边的下一行
Class<?> clazz = TypeUtils.loadClass(typeName, config.getDefaultClassLoader());
就是加载制定的类以便于反序列化
加载指定类
在上面用loadlass加载完类后,继续这个if代码块,在最下面有一个
1 2
| ObjectDeserializer deserializer = config.getDeserializer(clazz); return deserializer.deserialze(this, clazz, fieldName);
|
这边是为了获取反序列化器deserializer,跟进getDeserializer,因为type instanceof Class<?>
,跳到 getDeserializer((Class<?>) type, type)
中,
在这个getDeserializer,会经过一些替换处理,if判断,比如说会把$
换成.
- className = className.replace(‘$’, ‘.’);
会检测是否java.awt.
开头等等。
最后因为传入的value是org.example.user,流程会来到461行的
derializer = createJavaBeanDeserializer(clazz, type);
跟进createJavaBeanDeserializer(),这个函数里的判断主要是围绕 asmEnable 变量的true和false来进行的,在一系列的处理后,来到了
1
| JavaBeanInfo beanInfo = JavaBeanInfo.build(clazz, type, propertyNamingStrategy);
|
跟进这个build()函数,这函数内容也非常多,不过因为两个长的 if 代码块不会运行,所以可以忽略,重点是下面的三个 for 代码块,
其中第一个循环 for (Method method : methods)
是在指定的java类中找setter类方法。那些“函数命名以set开头,第四个字母大写”之类的条件,应该就是在这里进行判断处理的。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65
| String methodName = method.getName(); if (methodName.length() < 4) { continue; }
if (Modifier.isStatic(method.getModifiers())) { continue; }
if (!(method.getReturnType().equals(Void.TYPE) || method.getReturnType().equals(method.getDeclaringClass()))) { continue; } Class<?>[] types = method.getParameterTypes(); if (types.length != 1) { continue; }
JSONField annotation = method.getAnnotation(JSONField.class);
if (annotation == null) { annotation = TypeUtils.getSuperMethodAnnotation(clazz, method); }
if (annotation != null) { if (!annotation.deserialize()) { continue; }
ordinal = annotation.ordinal(); serialzeFeatures = SerializerFeature.of(annotation.serialzeFeatures()); parserFeatures = Feature.of(annotation.parseFeatures());
if (annotation.name().length() != 0) { String propertyName = annotation.name(); add(fieldList, new FieldInfo(propertyName, method, null, clazz, type, ordinal, serialzeFeatures, parserFeatures, annotation, null, null)); continue; } }
if (!methodName.startsWith("set")) { continue; }
char c3 = methodName.charAt(3);
String propertyName; if (Character.isUpperCase(c3) || c3 > 512 ) { 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; }
|
在处理的最后,会使用add()将找到的方法加进fieldList中,
1 2
| add(fieldList, new FieldInfo(propertyName, method, field, clazz, type, ordinal, serialzeFeatures, parserFeatures, annotation, fieldAnnotation, null));
|
第二个for (Field field : clazz.getFields())
是遍历所有public的方法,
第三个与第一个类似,不过是getter方法
getonly参数
在构造函数FieldInfo()中,有一个东西需要注意,getonly参数
为了后续方便调试,这个参数需要为true,在FieldInfo()中,这个参数为true,需要 method.getParameterTypes()的长度不为1,
1
| if ((types = method.getParameterTypes()).length == 1)
|
然而在第一个for (Method method : methods) 循环中,有这样一个判断
1 2 3 4
| Class<?>[] types = method.getParameterTypes(); if (types.length != 1) { continue; }
|
所以在第一个for循环中,是永远不能使getonly为true的。因为如果参数不为1,那就会被continue,代码根本走不到add();如果参数为1,代码在走到了add(),那getonly就只能为false了。
所以需要看第二个for循环。
第二个for循环是找getter函数,构造的函数需要满足以下几个条件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33
| if (methodName.length() < 4) { continue; }
if (Modifier.isStatic(method.getModifiers())) { continue; }
if (methodName.startsWith("get") && Character.isUpperCase(methodName.charAt(3))) { if (method.getParameterTypes().length != 0) { continue; }
if (Collection.class.isAssignableFrom(method.getReturnType()) || Map.class.isAssignableFrom(method.getReturnType()) || AtomicBoolean.class == method.getReturnType() || AtomicInteger.class == method.getReturnType() || AtomicLong.class == method.getReturnType() ) { …………
FieldInfo fieldInfo = getField(fieldList, propertyName); if (fieldInfo != null) { continue; } ………… add(fieldList, new FieldInfo(propertyName, method, null, clazz, type, 0, 0, 0, annotation, null, null)); ………… }
|
所以为了试getonly变量为true,所需要构造的getter函数就可以简单写为
1 2 3 4 5 6 7 8 9 10 11
| public AtomicBoolean getKey2(){
AtomicBoolean re = null;
System.out.println("user调用了setKey2"); this.key1 = key1; this.key2 = key2; return re; }
|
之后,代码跳出JavaBeanInfo beanInfo = JavaBeanInfo.build(clazz, type, propertyNamingStrategy);
向下走到
1 2 3 4 5 6 7 8 9 10 11
| for (FieldInfo fieldInfo : beanInfo.fields) { if (fieldInfo.getOnly) { asmEnable = false; break; } ………… } ………… if (!asmEnable) { return new JavaBeanDeserializer(this, clazz, type); }
|
然后就可以将asmEnable设置为false,随后 return new JavaBeanDeserializer(this, clazz, type);
结束函数。
在return的时候,是new了一个新的JavaBeanDeserializer,所以还会在走一边之前的三个for循环那块的流程,然后退出到ParseConfig.java的derializer = createJavaBeanDeserializer(clazz, type);
,再继续退出当前的getDeserializer()函数,回到getDeserializer(),最后退出到DefaultJSONParser.java的
1 2
| ObjectDeserializer deserializer = config.getDeserializer(clazz); return deserializer.deserialze(this, clazz, fieldName);
|
反序列化
通过调整getter函数,使用ObjectDeserializer deserializer = config.getDeserializer(clazz);
获取到反序列化器后
退出到DefaultJSONParser.java的return deserializer.deserialze(this, clazz, fieldName);
后,
跟进deserialze()方法,跳过一些重载方法后,来到JavaBeanDeserializer.java文件中。走到object = createInstance(parser, type);
时。这里是新建一个实例,在这里面有一句object = constructor.newInstance();
会执行指定了类的构造方法
之后在fieldDeser.setValue(object, fieldValue);
中跟进,里面在经过层层if判断和其他的处理后,有一行反射代码method.invoke(object, value);
在这执行了之前加入到fieldList的setter函数,
这里是setName
而被执行的getter方法,是在JSON.java的return (JSONObject) JSON.toJSON(obj);
中被调用的,在使用parse()将字符串反序列化为对象后,判断对象类型是否是JSON,如果不是,就再使用toJSON()函数处理一下obj。
在toJSON中,通过ObjectSerializer serializer = config.getObjectWriter(clazz);
获取ObjectSerializer:
在获取到ObjectSerializer后,在Map<String, Object> values = javaBeanSerializer.getFieldValuesMap(javaObject);
里面执行的getter方法,具体来看:
getFieldValuesMap() –> getter.getPropertyValue(object) -> Object propertyValue = fieldInfo.get(object);
1 2 3 4 5
| public Object get(Object javaObject) throws IllegalAccessException, InvocationTargetException { if (method != null) { Object value = method.invoke(javaObject, new Object[0]); return value; }
|
测试执行命令
假如说在代码中有一个这样的类
1 2 3 4 5 6 7 8 9
| package org.example;
import java.io.IOException;
public class testcmd { public void setCmd(String cmd) throws IOException { Runtime.getRuntime().exec(cmd); } }
|
如果想要通过fastjson执行命令,可以这样写
1 2 3 4
| public static void test7(){ String cmd = "{\"@type\":\"org.example.testcmd\",\"cmd\":\"gnome-calculator\"}"; Object P2 = JSON.parseObject(cmd); }
|
寻找利用链
没搞明白,mgj
1.2.25
之前的流程相同,在检测到DEFAULT_TYPE_KEY(@type)后,多了这一行判断
1
| Class<?> clazz = config.checkAutoType(typeName, null);
|
参考
https://tttang.com/archive/1579/#toc__2
https://www.bilibili.com/video/BV1bD4y117Qh/
https://www.cnblogs.com/nice0e3/p/14601670.html